Fix optimistic effect deletedAt (#7606)

In this PR, I'm fixing part of the impact of soft deletion on optimistic
rendering.

## Backend Vision

1) Backend endpoints will not return soft deleted records (having
deletedAt set) by default. To get the softDeleted records, we will pass
a { withSoftDelete: true } additional param in the query.
2) Record relations will NEVER contain softDeleted relations

## Backend current state

Right now, we have the following behavior:
- if the query filters do not mention deletedAt, we don't return
softDeletedRecords
- if the query filters mention deletedAt, we take it into consideration.
Meaning that if we want to have the softDeleted records in any way we
need to do { or: [ deletedAt: NULL, deletedAt: NOT_NULL] }

## Optimistic rendering strategy

1) useDestroyOne/Many is triggering destroyOptimisticEffects (previously
deleteOptimisticEffects)
2) UseDeleteOne/Many and useRestoreOne/Many are actually triggering
updateOptimisticEffects (as they only update deletedAt field) AND we
need updateOptimisticEffects to take into account deletedAt (future
withSoftDelete: true) filter.
This commit is contained in:
Charles Bochet 2024-10-11 20:23:01 +02:00 committed by GitHub
parent d350143c92
commit 7b96be6f8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 406 additions and 256 deletions

View File

@ -7,15 +7,15 @@ import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/is
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const triggerDeleteRecordsOptimisticEffect = ({ export const triggerDestroyRecordsOptimisticEffect = ({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete, recordsToDestroy,
objectMetadataItems, objectMetadataItems,
}: { }: {
cache: ApolloCache<unknown>; cache: ApolloCache<unknown>;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
recordsToDelete: RecordGqlNode[]; recordsToDestroy: RecordGqlNode[];
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
}) => { }) => {
cache.modify<StoreObject>({ cache.modify<StoreObject>({
@ -36,7 +36,7 @@ export const triggerDeleteRecordsOptimisticEffect = ({
const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse; const rootQueryCachedObjectRecordConnection = rootQueryCachedResponse;
const recordIdsToDelete = recordsToDelete.map(({ id }) => id); const recordIdsToDelete = recordsToDestroy.map(({ id }) => id);
const cachedEdges = readField<RecordGqlRefEdge[]>( const cachedEdges = readField<RecordGqlRefEdge[]>(
'edges', 'edges',
@ -69,20 +69,15 @@ export const triggerDeleteRecordsOptimisticEffect = ({
}, },
}); });
recordsToDelete.forEach((recordToDelete) => { recordsToDestroy.forEach((recordToDestroy) => {
triggerUpdateRelationsOptimisticEffect({ triggerUpdateRelationsOptimisticEffect({
cache, cache,
sourceObjectMetadataItem: objectMetadataItem, sourceObjectMetadataItem: objectMetadataItem,
currentSourceRecord: recordToDelete, currentSourceRecord: recordToDestroy,
updatedSourceRecord: null, updatedSourceRecord: null,
objectMetadataItems, objectMetadataItems,
}); });
cache.modify({ cache.evict({ id: cache.identify(recordToDestroy) });
id: cache.identify(recordToDelete),
fields: {
deletedAt: () => recordToDelete.deletedAt,
},
});
}); });
}; };

View File

@ -65,48 +65,43 @@ export const triggerUpdateRecordOptimisticEffect = ({
const rootQueryFilter = rootQueryVariables?.filter; const rootQueryFilter = rootQueryVariables?.filter;
const rootQueryOrderBy = rootQueryVariables?.orderBy; const rootQueryOrderBy = rootQueryVariables?.orderBy;
const shouldTryToMatchFilter = isDefined(rootQueryFilter); const updatedRecordMatchesThisRootQueryFilter = isRecordMatchingFilter({
record: updatedRecord,
filter: rootQueryFilter ?? {},
objectMetadataItem,
});
if (shouldTryToMatchFilter) { const updatedRecordIndexInRootQueryEdges =
const updatedRecordMatchesThisRootQueryFilter = rootQueryCurrentEdges.findIndex(
isRecordMatchingFilter({ (cachedEdge) =>
record: updatedRecord, readField('id', cachedEdge.node) === updatedRecord.id,
filter: rootQueryFilter, );
objectMetadataItem,
const updatedRecordFoundInRootQueryEdges =
updatedRecordIndexInRootQueryEdges > -1;
const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
!updatedRecordFoundInRootQueryEdges;
const updatedRecordShouldBeRemovedFromRootQueryEdges =
!updatedRecordMatchesThisRootQueryFilter &&
updatedRecordFoundInRootQueryEdges;
if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord);
if (isDefined(updatedRecordNodeReference)) {
rootQueryNextEdges.push({
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
node: updatedRecordNodeReference,
cursor: '',
}); });
const updatedRecordIndexInRootQueryEdges =
rootQueryCurrentEdges.findIndex(
(cachedEdge) =>
readField('id', cachedEdge.node) === updatedRecord.id,
);
const updatedRecordFoundInRootQueryEdges =
updatedRecordIndexInRootQueryEdges > -1;
const updatedRecordShouldBeAddedToRootQueryEdges =
updatedRecordMatchesThisRootQueryFilter &&
!updatedRecordFoundInRootQueryEdges;
const updatedRecordShouldBeRemovedFromRootQueryEdges =
!updatedRecordMatchesThisRootQueryFilter &&
updatedRecordFoundInRootQueryEdges;
if (updatedRecordShouldBeAddedToRootQueryEdges) {
const updatedRecordNodeReference = toReference(updatedRecord);
if (isDefined(updatedRecordNodeReference)) {
rootQueryNextEdges.push({
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
node: updatedRecordNodeReference,
cursor: '',
});
}
} }
}
if (updatedRecordShouldBeRemovedFromRootQueryEdges) { if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1); rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
}
} }
const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy); const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);

View File

@ -1,7 +1,7 @@
import { ApolloCache } from '@apollo/client'; import { ApolloCache } from '@apollo/client';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect'; import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect'; import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach'; import { CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH } from '@/apollo/types/coreObjectNamesToDeleteOnRelationDetach';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -122,10 +122,10 @@ export const triggerUpdateRelationsOptimisticEffect = ({
); );
if (shouldCascadeDeleteTargetRecords) { if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem: fullTargetObjectMetadataItem, objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDelete: targetRecordsToDetachFrom, recordsToDestroy: targetRecordsToDetachFrom,
objectMetadataItems, objectMetadataItems,
}); });
} else { } else {

View File

@ -18,11 +18,11 @@ export const useDeleteRecordFromCache = ({
const { objectMetadataItems } = useObjectMetadataItems(); const { objectMetadataItems } = useObjectMetadataItems();
return (recordToDelete: ObjectRecord) => { return (recordToDestroy: ObjectRecord) => {
deleteRecordFromCache({ deleteRecordFromCache({
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordToDelete, recordToDestroy,
cache: apolloClient.cache, cache: apolloClient.cache,
}); });
}; };

View File

@ -1,6 +1,6 @@
import { ApolloCache } from '@apollo/client'; import { ApolloCache } from '@apollo/client';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
@ -8,21 +8,21 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const deleteRecordFromCache = ({ export const deleteRecordFromCache = ({
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordToDelete, recordToDestroy,
cache, cache,
}: { }: {
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[]; objectMetadataItems: ObjectMetadataItem[];
recordToDelete: ObjectRecord; recordToDestroy: ObjectRecord;
cache: ApolloCache<object>; cache: ApolloCache<object>;
}) => { }) => {
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,
recordsToDelete: [ recordsToDestroy: [
{ {
...recordToDelete, ...recordToDestroy,
__typename: getObjectTypename(objectMetadataItem.nameSingular), __typename: getObjectTypename(objectMetadataItem.nameSingular),
}, },
], ],

View File

@ -1,4 +1,4 @@
import { act, renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { import {
query, query,
@ -6,9 +6,10 @@ import {
variables, variables,
} from '@/object-record/hooks/__mocks__/useDeleteManyRecords'; } from '@/object-record/hooks/__mocks__/useDeleteManyRecords';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { act } from 'react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const people = [ const personIds = [
'a7286b9a-c039-4a89-9567-2dfa7953cda9', 'a7286b9a-c039-4a89-9567-2dfa7953cda9',
'37faabcd-cb39-4a0a-8618-7e3fda9afca0', '37faabcd-cb39-4a0a-8618-7e3fda9afca0',
]; ];
@ -41,7 +42,7 @@ describe('useDeleteManyRecords', () => {
); );
await act(async () => { await act(async () => {
const res = await result.current.deleteManyRecords(people); const res = await result.current.deleteManyRecords(personIds);
expect(res).toBeDefined(); expect(res).toBeDefined();
expect(res[0]).toHaveProperty('id'); expect(res[0]).toHaveProperty('id');
}); });

View File

@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
@ -122,19 +122,19 @@ export const useCreateManyRecords = <
}, },
}) })
.catch((error: Error) => { .catch((error: Error) => {
recordsCreatedInCache.forEach((recordToDelete) => { recordsCreatedInCache.forEach((recordToDestroy) => {
deleteRecordFromCache({ deleteRecordFromCache({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
cache: apolloClient.cache, cache: apolloClient.cache,
recordToDelete, recordToDestroy,
}); });
}); });
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache: apolloClient.cache, cache: apolloClient.cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: recordsCreatedInCache, recordsToDestroy: recordsCreatedInCache,
objectMetadataItems, objectMetadataItems,
}); });

View File

@ -3,7 +3,7 @@ import { useState } from 'react';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
@ -118,13 +118,13 @@ export const useCreateOneRecord = <
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
cache: apolloClient.cache, cache: apolloClient.cache,
recordToDelete: recordCreatedInCache, recordToDestroy: recordCreatedInCache,
}); });
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache: apolloClient.cache, cache: apolloClient.cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: [recordCreatedInCache], recordsToDestroy: [recordCreatedInCache],
objectMetadataItems, objectMetadataItems,
}); });

View File

@ -1,14 +1,15 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState'; import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -62,49 +63,74 @@ export const useDeleteManyRecords = ({
const deletedRecords = []; const deletedRecords = [];
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) { for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
const batchIds = idsToDelete.slice( const batchedIdsToDelete = idsToDelete.slice(
batchIndex * mutationPageSize, batchIndex * mutationPageSize,
(batchIndex + 1) * mutationPageSize, (batchIndex + 1) * mutationPageSize,
); );
const currentTimestamp = new Date().toISOString();
const cachedRecords = batchedIdsToDelete
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
.filter(isDefined);
if (!options?.skipOptimisticEffect) {
cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord || !cachedRecord.id) {
return;
}
const cachedRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
const computedOptimisticRecord = {
...cachedRecord,
...{ id: cachedRecord.id, deletedAt: currentTimestamp },
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
return null;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: computedOptimisticRecord,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
});
}
const deletedRecordsResponse = await apolloClient const deletedRecordsResponse = await apolloClient
.mutate({ .mutate({
mutation: deleteManyRecordsMutation, mutation: deleteManyRecordsMutation,
variables: { variables: {
filter: { id: { in: batchIds } }, filter: { id: { in: batchedIdsToDelete } },
}, },
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: batchIds.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
})),
},
update: options?.skipOptimisticEffect
? undefined
: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length) return;
const cachedRecords = records
.map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined);
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDelete: cachedRecords,
objectMetadataItems,
});
},
}) })
.catch((error: Error) => { .catch((error: Error) => {
const cachedRecords = batchIds.map((idToDelete) =>
getRecordFromCache(idToDelete, apolloClient.cache),
);
cachedRecords.forEach((cachedRecord) => { cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord) { if (!cachedRecord) {
return; return;
@ -114,23 +140,45 @@ export const useDeleteManyRecords = ({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
cache: apolloClient.cache, cache: apolloClient.cache,
record: { record: cachedRecord,
...cachedRecord,
deletedAt: null,
},
}); });
});
triggerCreateRecordsOptimisticEffect({ const cachedRecordWithConnection =
cache: apolloClient.cache, getRecordNodeFromRecord<ObjectRecord>({
objectMetadataItem, record: cachedRecord,
objectMetadataItems, objectMetadataItem,
recordsToCreate: cachedRecords objectMetadataItems,
.filter(isDefined) computeReferences: true,
.map((cachedRecord) => ({ });
...cachedRecord,
deletedAt: null, const computedOptimisticRecord = {
})), ...cachedRecord,
...{ id: cachedRecord.id, deletedAt: currentTimestamp },
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
if (
!optimisticRecordWithConnection ||
!cachedRecordWithConnection
) {
return null;
}
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
}); });
throw error; throw error;

View File

@ -1,13 +1,14 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
@ -41,66 +42,85 @@ export const useDeleteOneRecord = ({
async (idToDelete: string) => { async (idToDelete: string) => {
const currentTimestamp = new Date().toISOString(); const currentTimestamp = new Date().toISOString();
const cachedRecord = getRecordFromCache(idToDelete, apolloClient.cache);
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
const computedOptimisticRecord = {
...cachedRecord,
...{ id: idToDelete, deletedAt: currentTimestamp },
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
return null;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: computedOptimisticRecord,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
const deletedRecord = await apolloClient const deletedRecord = await apolloClient
.mutate({ .mutate({
mutation: deleteOneRecordMutation, mutation: deleteOneRecordMutation,
variables: { variables: {
idToDelete: idToDelete, idToDelete: idToDelete,
}, },
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDelete,
deletedAt: currentTimestamp,
},
},
update: (cache, { data }) => { update: (cache, { data }) => {
const record = data?.[mutationResponseField]; const record = data?.[mutationResponseField];
if (!record) return; if (!record || !cachedRecord) return;
const cachedRecord = getRecordFromCache(record.id, cache); triggerUpdateRecordOptimisticEffect({
if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: [cachedRecord], currentRecord: cachedRecord,
updatedRecord: record,
objectMetadataItems, objectMetadataItems,
}); });
}, },
}) })
.catch((error: Error) => { .catch((error: Error) => {
const cachedRecord = getRecordFromCache(
idToDelete,
apolloClient.cache,
);
if (!cachedRecord) { if (!cachedRecord) {
throw error; throw error;
} }
updateRecordFromCache({ updateRecordFromCache({
objectMetadataItems, objectMetadataItems,
objectMetadataItem, objectMetadataItem,
cache: apolloClient.cache, cache: apolloClient.cache,
record: { record: cachedRecord,
...cachedRecord,
deletedAt: null,
},
}); });
triggerCreateRecordsOptimisticEffect({ triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache, cache: apolloClient.cache,
objectMetadataItem, objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems, objectMetadataItems,
recordsToCreate: [
{
...cachedRecord,
deletedAt: null,
},
],
}); });
throw error; throw error;
@ -114,7 +134,6 @@ export const useDeleteOneRecord = ({
getRecordFromCache, getRecordFromCache,
mutationResponseField, mutationResponseField,
objectMetadataItem, objectMetadataItem,
objectNameSingular,
objectMetadataItems, objectMetadataItems,
], ],
); );

View File

@ -1,7 +1,7 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState'; import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@ -61,12 +61,12 @@ export const useDestroyManyRecords = ({
const destroyedRecords = []; const destroyedRecords = [];
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) { for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
const batchIds = idsToDestroy.slice( const batchedIdToDestroy = idsToDestroy.slice(
batchIndex * mutationPageSize, batchIndex * mutationPageSize,
(batchIndex + 1) * mutationPageSize, (batchIndex + 1) * mutationPageSize,
); );
const originalRecords = idsToDestroy const originalRecords = batchedIdToDestroy
.map((recordId) => getRecordFromCache(recordId, apolloClient.cache)) .map((recordId) => getRecordFromCache(recordId, apolloClient.cache))
.filter(isDefined); .filter(isDefined);
@ -74,15 +74,17 @@ export const useDestroyManyRecords = ({
.mutate({ .mutate({
mutation: destroyManyRecordsMutation, mutation: destroyManyRecordsMutation,
variables: { variables: {
filter: { id: { in: batchIds } }, filter: { id: { in: batchedIdToDestroy } },
}, },
optimisticResponse: options?.skipOptimisticEffect optimisticResponse: options?.skipOptimisticEffect
? undefined ? undefined
: { : {
[mutationResponseField]: batchIds.map((idToDestroy) => ({ [mutationResponseField]: batchedIdToDestroy.map(
__typename: capitalize(objectNameSingular), (idToDestroy) => ({
id: idToDestroy, __typename: capitalize(objectNameSingular),
})), id: idToDestroy,
}),
),
}, },
update: options?.skipOptimisticEffect update: options?.skipOptimisticEffect
? undefined ? undefined
@ -95,10 +97,10 @@ export const useDestroyManyRecords = ({
.map((record) => getRecordFromCache(record.id, cache)) .map((record) => getRecordFromCache(record.id, cache))
.filter(isDefined); .filter(isDefined);
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: cachedRecords, recordsToDestroy: cachedRecords,
objectMetadataItems, objectMetadataItems,
}); });
}, },

View File

@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
@ -65,10 +65,10 @@ export const useDestroyOneRecord = ({
if (!cachedRecord) return; if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: [cachedRecord], recordsToDestroy: [cachedRecord],
objectMetadataItems, objectMetadataItems,
}); });
}, },

View File

@ -1,9 +1,15 @@
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { apiConfigState } from '@/client-config/states/apiConfigState'; import { apiConfigState } from '@/client-config/states/apiConfigState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation'; import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField'; import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -34,10 +40,16 @@ export const useRestoreManyRecords = ({
objectNameSingular, objectNameSingular,
}); });
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular,
});
const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({ const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({
objectNameSingular, objectNameSingular,
}); });
const { objectMetadataItems } = useObjectMetadataItems();
const mutationResponseField = getRestoreManyRecordsMutationResponseField( const mutationResponseField = getRestoreManyRecordsMutationResponseField(
objectMetadataItem.namePlural, objectMetadataItem.namePlural,
); );
@ -51,36 +63,124 @@ export const useRestoreManyRecords = ({
const restoredRecords = []; const restoredRecords = [];
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) { for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
const batchIds = idsToRestore.slice( const batchedIdsToRestore = idsToRestore.slice(
batchIndex * mutationPageSize, batchIndex * mutationPageSize,
(batchIndex + 1) * mutationPageSize, (batchIndex + 1) * mutationPageSize,
); );
// TODO: fix optimistic effect const cachedRecords = batchedIdsToRestore
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`; .map((idToRestore) =>
const findManyQueryName = `FindMany${capitalize( getRecordFromCache(idToRestore, apolloClient.cache),
objectMetadataItem.namePlural, )
)}`; .filter(isDefined);
if (!options?.skipOptimisticEffect) {
cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord || !cachedRecord.id) {
return;
}
const cachedRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
const computedOptimisticRecord = {
...cachedRecord,
...{ id: cachedRecord.id, deletedAt: null },
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
if (!optimisticRecordWithConnection || !cachedRecordWithConnection) {
return null;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: computedOptimisticRecord,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
});
}
const restoredRecordsResponse = await apolloClient const restoredRecordsResponse = await apolloClient
.mutate({ .mutate({
mutation: restoreManyRecordsMutation, mutation: restoreManyRecordsMutation,
refetchQueries: [findOneQueryName, findManyQueryName],
variables: { variables: {
filter: { id: { in: batchIds } }, filter: { id: { in: batchedIdsToRestore } },
}, },
optimisticResponse: options?.skipOptimisticEffect
? undefined
: {
[mutationResponseField]: batchIds.map((idToRestore) => ({
__typename: capitalize(objectNameSingular),
id: idToRestore,
deletedAt: null,
})),
},
}) })
.catch((error: Error) => { .catch((error: Error) => {
// TODO: revert optimistic effect (once optimistic effect is fixed) cachedRecords.forEach((cachedRecord) => {
if (!cachedRecord) {
return;
}
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloClient.cache,
record: cachedRecord,
});
const cachedRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
const computedOptimisticRecord = {
...cachedRecord,
...{ id: cachedRecord.id, deletedAt: null },
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
computeReferences: true,
});
if (
!optimisticRecordWithConnection ||
!cachedRecordWithConnection
) {
return null;
}
triggerUpdateRecordOptimisticEffect({
cache: apolloClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
});
throw error; throw error;
}); });

View File

@ -11,6 +11,7 @@ import {
EmailsFilter, EmailsFilter,
FloatFilter, FloatFilter,
FullNameFilter, FullNameFilter,
LeafObjectRecordFilter,
LinksFilter, LinksFilter,
NotObjectRecordFilter, NotObjectRecordFilter,
OrObjectRecordFilter, OrObjectRecordFilter,
@ -29,6 +30,12 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { isEmptyObject } from '~/utils/isEmptyObject'; import { isEmptyObject } from '~/utils/isEmptyObject';
const isLeafFilter = (
filter: RecordGqlOperationFilter,
): filter is LeafObjectRecordFilter => {
return !isAndFilter(filter) && !isOrFilter(filter) && !isNotFilter(filter);
};
const isAndFilter = ( const isAndFilter = (
filter: RecordGqlOperationFilter, filter: RecordGqlOperationFilter,
): filter is AndObjectRecordFilter => 'and' in filter && !!filter.and; ): filter is AndObjectRecordFilter => 'and' in filter && !!filter.and;
@ -50,7 +57,7 @@ export const isRecordMatchingFilter = ({
filter: RecordGqlOperationFilter; filter: RecordGqlOperationFilter;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}): boolean => { }): boolean => {
if (Object.keys(filter).length === 0) { if (Object.keys(filter).length === 0 && record.deletedAt === null) {
return true; return true;
} }
@ -120,6 +127,12 @@ export const isRecordMatchingFilter = ({
); );
} }
if (isLeafFilter(filter)) {
if (isDefined(record.deletedAt) && filter.deletedAt === undefined) {
return false;
}
}
return Object.entries(filter).every(([filterKey, filterValue]) => { return Object.entries(filter).every(([filterKey, filterValue]) => {
if (!isDefined(filterValue)) { if (!isDefined(filterValue)) {
throw new Error( throw new Error(

View File

@ -1,11 +1,7 @@
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { useRecoilValue } from 'recoil';
type UseDeleteTableDataOptions = Pick< type UseDeleteTableDataOptions = Pick<
UseTableDataOptions, UseTableDataOptions,
@ -16,41 +12,16 @@ export const useDeleteTableData = ({
objectNameSingular, objectNameSingular,
recordIndexId, recordIndexId,
}: UseDeleteTableDataOptions) => { }: UseDeleteTableDataOptions) => {
const { fetchAllRecordIds } = useFetchAllRecordIds({ const { resetTableRowSelection } = useRecordTable({
objectNameSingular, recordTableId: recordIndexId,
}); });
const { resetTableRowSelection, hasUserSelectedAllRowsState } =
useRecordTable({
recordTableId: recordIndexId,
});
const tableRowIds = useRecoilValue(
tableRowIdsComponentState({
scopeId: getScopeIdFromComponentId(recordIndexId),
}),
);
const { deleteManyRecords } = useDeleteManyRecords({ const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular, objectNameSingular,
}); });
const { favorites, deleteFavorite } = useFavorites(); const { favorites, deleteFavorite } = useFavorites();
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
const deleteRecords = async (recordIdsToDelete: string[]) => { const deleteRecords = async (recordIdsToDelete: string[]) => {
if (hasUserSelectedAllRows) {
const allRecordIds = await fetchAllRecordIds();
const unselectedRecordIds = tableRowIds.filter(
(recordId) => !recordIdsToDelete.includes(recordId),
);
recordIdsToDelete = allRecordIds.filter(
(recordId) => !unselectedRecordIds.includes(recordId),
);
}
resetTableRowSelection(); resetTableRowSelection();
for (const recordIdToDelete of recordIdsToDelete) { for (const recordIdToDelete of recordIdsToDelete) {

View File

@ -11,7 +11,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { Dropdown } from '../../dropdown/components/Dropdown'; import { Dropdown } from '../../dropdown/components/Dropdown';
@ -35,7 +35,7 @@ export const ShowPageMoreButton = ({
const { deleteOneRecord } = useDeleteOneRecord({ const { deleteOneRecord } = useDeleteOneRecord({
objectNameSingular, objectNameSingular,
}); });
const { destroyManyRecords } = useDestroyManyRecords({ const { destroyOneRecord } = useDestroyOneRecord({
objectNameSingular, objectNameSingular,
}); });
const { restoreManyRecords } = useRestoreManyRecords({ const { restoreManyRecords } = useRestoreManyRecords({
@ -48,7 +48,7 @@ export const ShowPageMoreButton = ({
}; };
const handleDestroy = () => { const handleDestroy = () => {
destroyManyRecords([recordId]); destroyOneRecord(recordId);
closeDropdown(); closeDropdown();
navigate(navigationMemorizedUrl, { replace: true }); navigate(navigationMemorizedUrl, { replace: true });
}; };

View File

@ -1,15 +1,15 @@
import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
@ -24,7 +24,7 @@ export const usePersistViewFilterRecords = () => {
objectNameSingular: CoreObjectNameSingular.ViewFilter, objectNameSingular: CoreObjectNameSingular.ViewFilter,
}); });
const { deleteOneRecordMutation } = useDeleteOneRecordMutation({ const { destroyOneRecordMutation } = useDestroyOneRecordMutation({
objectNameSingular: CoreObjectNameSingular.ViewFilter, objectNameSingular: CoreObjectNameSingular.ViewFilter,
}); });
@ -129,12 +129,12 @@ export const usePersistViewFilterRecords = () => {
return Promise.all( return Promise.all(
viewFilterIdsToDelete.map((viewFilterId) => viewFilterIdsToDelete.map((viewFilterId) =>
apolloClient.mutate({ apolloClient.mutate({
mutation: deleteOneRecordMutation, mutation: destroyOneRecordMutation,
variables: { variables: {
idToDelete: viewFilterId, idToDestroy: viewFilterId,
}, },
update: (cache, { data }) => { update: (cache, { data }) => {
const record = data?.['deleteViewFilter']; const record = data?.['destroyViewFilter'];
if (!record) return; if (!record) return;
@ -142,10 +142,10 @@ export const usePersistViewFilterRecords = () => {
if (!cachedRecord) return; if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: [cachedRecord], recordsToDestroy: [cachedRecord],
objectMetadataItems, objectMetadataItems,
}); });
}, },
@ -155,7 +155,7 @@ export const usePersistViewFilterRecords = () => {
}, },
[ [
apolloClient, apolloClient,
deleteOneRecordMutation, destroyOneRecordMutation,
getRecordFromCache, getRecordFromCache,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,

View File

@ -1,15 +1,15 @@
import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { triggerDestroyRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDestroyRecordsOptimisticEffect';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect'; import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { GraphQLView } from '@/views/types/GraphQLView'; import { GraphQLView } from '@/views/types/GraphQLView';
@ -24,7 +24,7 @@ export const usePersistViewSortRecords = () => {
objectNameSingular: CoreObjectNameSingular.ViewSort, objectNameSingular: CoreObjectNameSingular.ViewSort,
}); });
const { deleteOneRecordMutation } = useDeleteOneRecordMutation({ const { destroyOneRecordMutation } = useDestroyOneRecordMutation({
objectNameSingular: CoreObjectNameSingular.ViewSort, objectNameSingular: CoreObjectNameSingular.ViewSort,
}); });
@ -124,12 +124,12 @@ export const usePersistViewSortRecords = () => {
return Promise.all( return Promise.all(
viewSortIdsToDelete.map((viewSortId) => viewSortIdsToDelete.map((viewSortId) =>
apolloClient.mutate({ apolloClient.mutate({
mutation: deleteOneRecordMutation, mutation: destroyOneRecordMutation,
variables: { variables: {
idToDelete: viewSortId, idToDestroy: viewSortId,
}, },
update: (cache, { data }) => { update: (cache, { data }) => {
const record = data?.['deleteViewSort']; const record = data?.['destroyViewSort'];
if (!record) return; if (!record) return;
@ -137,10 +137,10 @@ export const usePersistViewSortRecords = () => {
if (!cachedRecord) return; if (!cachedRecord) return;
triggerDeleteRecordsOptimisticEffect({ triggerDestroyRecordsOptimisticEffect({
cache, cache,
objectMetadataItem, objectMetadataItem,
recordsToDelete: [cachedRecord], recordsToDestroy: [cachedRecord],
objectMetadataItems, objectMetadataItems,
}); });
}, },
@ -150,7 +150,7 @@ export const usePersistViewSortRecords = () => {
}, },
[ [
apolloClient, apolloClient,
deleteOneRecordMutation, destroyOneRecordMutation,
getRecordFromCache, getRecordFromCache,
objectMetadataItem, objectMetadataItem,
objectMetadataItems, objectMetadataItems,

View File

@ -82,62 +82,62 @@ export class GraphqlQueryFilterFieldParser {
switch (operator) { switch (operator) {
case 'eq': case 'eq':
return { return {
sql: `${objectNameSingular}.${key} = :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" = :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'neq': case 'neq':
return { return {
sql: `${objectNameSingular}.${key} != :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" != :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'gt': case 'gt':
return { return {
sql: `${objectNameSingular}.${key} > :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" > :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'gte': case 'gte':
return { return {
sql: `${objectNameSingular}.${key} >= :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" >= :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'lt': case 'lt':
return { return {
sql: `${objectNameSingular}.${key} < :${key}${uuid}`, sql: `"${objectNameSingular}".${key} < :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'lte': case 'lte':
return { return {
sql: `${objectNameSingular}.${key} <= :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" <= :${key}${uuid}`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'in': case 'in':
return { return {
sql: `${objectNameSingular}.${key} IN (:...${key}${uuid})`, sql: `"${objectNameSingular}"."${key}" IN (:...${key}${uuid})`,
params: { [`${key}${uuid}`]: value }, params: { [`${key}${uuid}`]: value },
}; };
case 'is': case 'is':
return { return {
sql: `${objectNameSingular}.${key} IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`, sql: `"${objectNameSingular}"."${key}" IS ${value === 'NULL' ? 'NULL' : 'NOT NULL'}`,
params: {}, params: {},
}; };
case 'like': case 'like':
return { return {
sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` }, params: { [`${key}${uuid}`]: `${value}` },
}; };
case 'ilike': case 'ilike':
return { return {
sql: `${objectNameSingular}.${key} ILIKE :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" ILIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` }, params: { [`${key}${uuid}`]: `${value}` },
}; };
case 'startsWith': case 'startsWith':
return { return {
sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` }, params: { [`${key}${uuid}`]: `${value}` },
}; };
case 'endsWith': case 'endsWith':
return { return {
sql: `${objectNameSingular}.${key} LIKE :${key}${uuid}`, sql: `"${objectNameSingular}"."${key}" LIKE :${key}${uuid}`,
params: { [`${key}${uuid}`]: `${value}` }, params: { [`${key}${uuid}`]: `${value}` },
}; };
default: default:

View File

@ -58,7 +58,7 @@ export class GraphqlQueryDestroyOneResolverService
); );
const nonFormattedDeletedObjectRecords = await queryBuilder const nonFormattedDeletedObjectRecords = await queryBuilder
.where({ .where(`"${objectMetadataMapItem.nameSingular}".id = :id`, {
id: args.id, id: args.id,
}) })
.take(1) .take(1)

View File

@ -16,6 +16,7 @@ import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/obj
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
@Injectable() @Injectable()
export class GraphqlQueryUpdateManyResolverService export class GraphqlQueryUpdateManyResolverService
@ -57,9 +58,14 @@ export class GraphqlQueryUpdateManyResolverService
objectMetadataMapItem.nameSingular, objectMetadataMapItem.nameSingular,
); );
const tableName = computeTableName(
objectMetadataMapItem.nameSingular,
objectMetadataMapItem.isCustom,
);
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
queryBuilder, queryBuilder,
objectMetadataMapItem.nameSingular, tableName,
args.filter, args.filter,
); );