perf: apply record optimistic effects with cache.modify on mutation (#3540)

* perf: apply record optimistic effects with cache.modify on mutation

Closes #3509

* refactor: return early when created records do not match filter

* fix: fix id generation on record creation

* fix: comment filtering behavior on record creation

* Fixed typing error

* refactor: review - use ??

* refactor: review - add variables in readFieldValueToSort

* docs: review - add comments for variables.first in triggerUpdateRecordOptimisticEffect

* refactor: review - add intermediary variable for 'not' filter in useMultiObjectSearchMatchesSearchFilterAndToSelectQuery

* refactor: review - add filter utils

* fix: fix tests

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Thaïs
2024-01-23 14:13:00 -03:00
committed by GitHub
parent 9ebc0deaaf
commit 014f11fb6f
57 changed files with 852 additions and 1118 deletions

View File

@ -1,117 +0,0 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { produce } from 'immer';
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';
export const getRecordOptimisticEffectDefinition = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}): OptimisticEffectDefinition => ({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
resolver: ({
currentCacheData: currentData,
createdRecords,
updatedRecords,
deletedRecordIds,
variables,
}) => {
const newRecordPaginatedCacheField = produce<ObjectRecordConnection<any>>(
currentData as ObjectRecordConnection<any>,
(draft) => {
const existingDataIsEmpty = !draft || !draft.edges || !draft.edges[0];
if (isNonEmptyArray(createdRecords)) {
if (existingDataIsEmpty) {
return {
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Connection`,
edges: createdRecords.map((createdRecord) => ({
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Edge`,
node: createdRecord,
cursor: '',
})),
pageInfo: {
endCursor: '',
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
},
};
} else {
for (const createdRecord of createdRecords) {
const existingRecord = draft.edges.find(
(edge) => edge.node.id === createdRecord.id,
);
if (existingRecord) {
existingRecord.node = createdRecord;
continue;
}
draft.edges.unshift({
node: createdRecord,
cursor: '',
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Edge`,
});
}
}
}
if (isNonEmptyArray(deletedRecordIds)) {
draft.edges = draft.edges.filter(
(edge) => !deletedRecordIds.includes(edge.node.id),
);
}
if (isNonEmptyArray(updatedRecords)) {
for (const updatedRecord of updatedRecords) {
const updatedRecordIsOutOfQueryFilter =
isDefined(variables.filter) &&
!isRecordMatchingFilter({
record: updatedRecord,
filter: variables.filter,
objectMetadataItem,
});
if (updatedRecordIsOutOfQueryFilter) {
draft.edges = draft.edges.filter(
(edge) => edge.node.id !== updatedRecord.id,
);
} else {
const foundUpdatedRecordInCacheQuery = draft.edges.find(
(edge) => edge.node.id === updatedRecord.id,
);
if (foundUpdatedRecordInCacheQuery) {
foundUpdatedRecordInCacheQuery.node = updatedRecord;
} else {
// TODO: add order by
draft.edges.push({
node: updatedRecord,
cursor: '',
__typename: `${capitalize(
objectMetadataItem.nameSingular,
)}Edge`,
});
}
}
}
}
},
);
return newRecordPaginatedCacheField;
},
objectMetadataItem,
});

View File

@ -1,77 +1,67 @@
import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
import { getCreateManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from '~/utils/string/capitalize';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
export const useCreateManyRecords = <T extends ObjectRecord>({
export const useCreateManyRecords = <
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
}: ObjectMetadataItemIdentifier) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, createManyRecordsMutation } =
useObjectMetadataItem({
objectNameSingular,
});
const { generateEmptyRecord } = useGenerateEmptyRecord({
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
objectMetadataItem,
});
const apolloClient = useApolloClient();
const createManyRecords = async (data: Partial<T>[]) => {
const withIds = data.map((record) => ({
...record,
id: (record.id as string) ?? v4(),
}));
const createManyRecords = async (data: Partial<CreatedObjectRecord>[]) => {
const optimisticallyCreatedRecords = data.map((record) =>
generateCachedObjectRecord<CreatedObjectRecord>(record),
);
withIds.forEach((record) => {
const emptyRecord: T | undefined = generateEmptyRecord({
id: record.id,
} as T);
const sanitizedCreateManyRecordsInput = data.map((input, index) =>
sanitizeRecordInput({
objectMetadataItem,
recordInput: { ...input, id: optimisticallyCreatedRecords[index].id },
}),
);
if (emptyRecord) {
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
createdRecords: [emptyRecord],
});
}
});
const mutationResponseField = getCreateManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
const createdObjects = await apolloClient.mutate({
mutation: createManyRecordsMutation,
variables: {
data: withIds,
data: sanitizedCreateManyRecordsInput,
},
optimisticResponse: {
[`create${capitalize(objectMetadataItem.namePlural)}`]: withIds.map(
(record) => generateEmptyRecord({ id: record.id }),
),
[mutationResponseField]: optimisticallyCreatedRecords,
},
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
records,
});
},
});
if (!createdObjects.data) {
return null;
}
const createdRecords =
createdObjects.data[
`create${capitalize(objectMetadataItem.namePlural)}`
] ?? [];
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
createdRecords,
});
return createdRecords as T[];
return createdObjects.data?.[mutationResponseField] ?? [];
};
return { createManyRecords };

View File

@ -1,73 +1,66 @@
import { useApolloClient } from '@apollo/client';
import { v4 } from 'uuid';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
import { useGenerateCachedObjectRecord } from '@/object-record/hooks/useGenerateCachedObjectRecord';
import { getCreateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { capitalize } from '~/utils/string/capitalize';
type useCreateOneRecordProps = {
objectNameSingular: string;
};
export const useCreateOneRecord = <T>({
export const useCreateOneRecord = <
CreatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
}: useCreateOneRecordProps) => {
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
{
objectNameSingular,
},
{ objectNameSingular },
);
// TODO: type this with a minimal type at least with Record<string, any>
const apolloClient = useApolloClient();
const { generateEmptyRecord } = useGenerateEmptyRecord({
const { generateCachedObjectRecord } = useGenerateCachedObjectRecord({
objectMetadataItem,
});
const createOneRecord = async (input: Record<string, any>) => {
const recordId = v4();
const createOneRecord = async (input: Partial<CreatedObjectRecord>) => {
const optimisticallyCreatedRecord =
generateCachedObjectRecord<CreatedObjectRecord>(input);
const generatedEmptyRecord = generateEmptyRecord({
id: recordId,
createdAt: new Date().toISOString(),
...input,
});
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
const sanitizedCreateOneRecordInput = sanitizeRecordInput({
objectMetadataItem,
recordInput: input,
recordInput: { ...input, id: optimisticallyCreatedRecord.id },
});
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
createdRecords: [generatedEmptyRecord],
});
const mutationResponseField =
getCreateOneRecordMutationResponseField(objectNameSingular);
const createdObject = await apolloClient.mutate({
mutation: createOneRecordMutation,
variables: {
input: { id: recordId, ...sanitizedUpdateOneRecordInput },
input: sanitizedCreateOneRecordInput,
},
optimisticResponse: {
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
generatedEmptyRecord,
[mutationResponseField]: optimisticallyCreatedRecord,
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!record) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
records: [record],
});
},
});
if (!createdObject.data) {
return null;
}
return createdObject.data[
`create${capitalize(objectMetadataItem.nameSingular)}`
] as T;
return createdObject.data?.[mutationResponseField] ?? null;
};
return {

View File

@ -1,9 +1,8 @@
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getDeleteManyRecordsMutationResponseField } from '@/object-record/hooks/useGenerateDeleteManyRecordMutation';
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { capitalize } from '~/utils/string/capitalize';
@ -12,39 +11,19 @@ type useDeleteOneRecordProps = {
refetchFindManyQuery?: boolean;
};
export const useDeleteManyRecords = <T>({
export const useDeleteManyRecords = ({
objectNameSingular,
refetchFindManyQuery = false,
}: useDeleteOneRecordProps) => {
const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const {
objectMetadataItem,
deleteManyRecordsMutation,
findManyRecordsQuery,
} = useObjectMetadataItem({
objectNameSingular,
});
const { objectMetadataItem, deleteManyRecordsMutation } =
useObjectMetadataItem({ objectNameSingular });
const apolloClient = useApolloClient();
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
const deleteManyRecords = async (idsToDelete: string[]) => {
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
deletedRecordIds: idsToDelete,
});
idsToDelete.forEach((idToDelete) => {
performOptimisticEvict(
capitalize(objectMetadataItem.nameSingular),
'id',
idToDelete,
);
});
const deleteRecordFilter: ObjectRecordQueryFilter = {
id: {
in: idsToDelete,
@ -56,14 +35,26 @@ export const useDeleteManyRecords = <T>({
filter: deleteRecordFilter,
// atMost: idsToDelete.length,
},
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
optimisticResponse: {
[mutationResponseField]: idsToDelete.map((idToDelete) => ({
__typename: capitalize(objectNameSingular),
id: idToDelete,
})),
},
update: (cache, { data }) => {
const records = data?.[mutationResponseField];
if (!records?.length) return;
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
records,
});
},
});
return deletedRecords.data[
`delete${capitalize(objectMetadataItem.namePlural)}`
] as T;
return deletedRecords.data?.[mutationResponseField] ?? null;
};
return { deleteManyRecords };

View File

@ -1,10 +1,9 @@
import { useCallback } from 'react';
import { useApolloClient } from '@apollo/client';
import { getOperationName } from '@apollo/client/utilities';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { useOptimisticEvict } from '@/apollo/optimistic-effect/hooks/useOptimisticEvict';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/generateDeleteOneRecordMutation';
import { capitalize } from '~/utils/string/capitalize';
type useDeleteOneRecordProps = {
@ -12,57 +11,50 @@ type useDeleteOneRecordProps = {
refetchFindManyQuery?: boolean;
};
export const useDeleteOneRecord = <T>({
export const useDeleteOneRecord = ({
objectNameSingular,
refetchFindManyQuery = false,
}: useDeleteOneRecordProps) => {
const { performOptimisticEvict } = useOptimisticEvict();
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
const { objectMetadataItem, deleteOneRecordMutation, findManyRecordsQuery } =
useObjectMetadataItem({
objectNameSingular,
});
const { objectMetadataItem, deleteOneRecordMutation } = useObjectMetadataItem(
{ objectNameSingular },
);
const apolloClient = useApolloClient();
const mutationResponseField =
getDeleteOneRecordMutationResponseField(objectNameSingular);
const deleteOneRecord = useCallback(
async (idToDelete: string) => {
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
deletedRecordIds: [idToDelete],
});
performOptimisticEvict(
capitalize(objectMetadataItem.nameSingular),
'id',
idToDelete,
);
const deletedRecord = await apolloClient.mutate({
mutation: deleteOneRecordMutation,
variables: {
idToDelete,
variables: { idToDelete },
optimisticResponse: {
[mutationResponseField]: {
__typename: capitalize(objectNameSingular),
id: idToDelete,
},
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!record) return;
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem,
records: [record],
});
},
refetchQueries: refetchFindManyQuery
? [getOperationName(findManyRecordsQuery) ?? '']
: [],
});
return deletedRecord.data[
`delete${capitalize(objectMetadataItem.nameSingular)}`
] as T;
return deletedRecord.data?.[mutationResponseField] ?? null;
},
[
triggerOptimisticEffects,
objectMetadataItem.nameSingular,
performOptimisticEvict,
apolloClient,
deleteOneRecordMutation,
refetchFindManyQuery,
findManyRecordsQuery,
mutationResponseField,
objectMetadataItem,
objectNameSingular,
],
);

View File

@ -6,14 +6,12 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useRecordOptimisticEffect } from '@/object-metadata/hooks/useRecordOptimisticEffect';
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
import { OrderByField } from '@/object-metadata/types/OrderByField';
import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords';
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection';
import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge';
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
import { filterUniqueRecordEdgesByCursor } from '@/object-record/utils/filterUniqueRecordEdgesByCursor';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -34,14 +32,12 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
onCompleted,
skip,
useRecordsWithoutConnection = false,
}: ObjectMetadataItemIdentifier & {
filter?: ObjectRecordQueryFilter;
orderBy?: OrderByField;
limit?: number;
onCompleted?: (data: ObjectRecordConnection<T>) => void;
skip?: boolean;
useRecordsWithoutConnection?: boolean;
}) => {
}: ObjectMetadataItemIdentifier &
ObjectRecordQueryVariables & {
onCompleted?: (data: ObjectRecordConnection<T>) => void;
skip?: boolean;
useRecordsWithoutConnection?: boolean;
}) => {
const findManyQueryStateIdentifier =
objectNameSingular +
JSON.stringify(filter) +
@ -64,13 +60,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
objectNameSingular,
});
useRecordOptimisticEffect({
objectMetadataItem,
filter,
orderBy,
limit,
});
const { enqueueSnackBar } = useSnackBar();
const currentWorkspace = useRecoilValue(currentWorkspaceState);

View File

@ -0,0 +1,41 @@
import { v4 } from 'uuid';
import { z } from 'zod';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
import { capitalize } from '~/utils/string/capitalize';
export const useGenerateCachedObjectRecord = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const generateCachedObjectRecord = <
GeneratedObjectRecord extends ObjectRecord,
>(
input: Record<string, unknown>,
) => {
const recordSchema = z.object(
Object.fromEntries(
objectMetadataItem.fields.map((fieldMetadataItem) => [
fieldMetadataItem.name,
z.unknown().default(generateEmptyFieldValue(fieldMetadataItem)),
]),
),
);
return {
__typename: capitalize(objectMetadataItem.nameSingular),
...recordSchema.parse({
id: v4(),
createdAt: new Date().toISOString(),
...input,
}),
} as GeneratedObjectRecord & { __typename: string };
};
return {
generateCachedObjectRecord,
};
};

View File

@ -5,6 +5,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getCreateManyRecordsMutationResponseField = (
objectNamePlural: string,
) => `create${capitalize(objectNamePlural)}`;
export const useGenerateCreateManyRecordMutation = ({
objectMetadataItem,
}: {
@ -16,11 +20,15 @@ export const useGenerateCreateManyRecordMutation = ({
return EMPTY_MUTATION;
}
const mutationResponseField = getCreateManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
return gql`
mutation Create${capitalize(
objectMetadataItem.namePlural,
)}($data: [${capitalize(objectMetadataItem.nameSingular)}CreateInput!]!) {
create${capitalize(objectMetadataItem.namePlural)}(data: $data) {
${mutationResponseField}(data: $data) {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))

View File

@ -5,6 +5,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getCreateOneRecordMutationResponseField = (
objectNameSingular: string,
) => `create${capitalize(objectNameSingular)}`;
export const useGenerateCreateOneRecordMutation = ({
objectMetadataItem,
}: {
@ -18,9 +22,13 @@ export const useGenerateCreateOneRecordMutation = ({
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const mutationResponseField = getCreateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
return gql`
mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) {
create${capitalizedObjectName}(data: $input) {
${mutationResponseField}(data: $input) {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))

View File

@ -4,6 +4,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getDeleteManyRecordsMutationResponseField = (
objectNamePlural: string,
) => `delete${capitalize(objectNamePlural)}`;
export const useGenerateDeleteManyRecordMutation = ({
objectMetadataItem,
}: {
@ -15,11 +19,15 @@ export const useGenerateDeleteManyRecordMutation = ({
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
const mutationResponseField = getDeleteManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
return gql`
mutation DeleteMany${capitalizedObjectName}($filter: ${capitalize(
objectMetadataItem.nameSingular,
)}FilterInput!) {
delete${capitalizedObjectName}(filter: $filter) {
${mutationResponseField}(filter: $filter) {
id
}
}

View File

@ -1,29 +0,0 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
export const useGenerateEmptyRecord = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
// Todo fix typing once we generate the return base on Metadata
const generateEmptyRecord = <T extends ObjectRecord>(input: T) => {
// Todo replace this by runtime typing
const validatedInput = input as T;
const emptyRecord = {} as any;
for (const fieldMetadataItem of objectMetadataItem.fields) {
emptyRecord[fieldMetadataItem.name] =
validatedInput[fieldMetadataItem.name] ??
generateEmptyFieldValue(fieldMetadataItem);
}
return emptyRecord as T;
};
return {
generateEmptyRecord,
};
};

View File

@ -5,13 +5,9 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getUpdateOneRecordMutationGraphQLField = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
return `update${capitalize(objectNameSingular)}`;
};
export const getUpdateOneRecordMutationResponseField = (
objectNameSingular: string,
) => `update${capitalize(objectNameSingular)}`;
export const useGenerateUpdateOneRecordMutation = ({
objectMetadataItem,
@ -26,14 +22,13 @@ export const useGenerateUpdateOneRecordMutation = ({
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const graphQLFieldForUpdateOneRecordMutation =
getUpdateOneRecordMutationGraphQLField({
objectNameSingular: objectMetadataItem.nameSingular,
});
const mutationResponseField = getUpdateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
return gql`
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
${graphQLFieldForUpdateOneRecordMutation}(id: $idToUpdate, data: $input) {
${mutationResponseField}(id: $idToUpdate, data: $input) {
id
${objectMetadataItem.fields
.map((field) => mapFieldMetadataToGraphQLQuery(field))

View File

@ -1,8 +1,8 @@
import { gql, useApolloClient } from '@apollo/client';
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from '~/utils/string/capitalize';
export const useGetRecordFromCache = ({
@ -13,9 +13,11 @@ export const useGetRecordFromCache = ({
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
const apolloClient = useApolloClient();
return (recordId: string) => {
return <CachedObjectRecord extends ObjectRecord = ObjectRecord>(
recordId: string,
) => {
if (!objectMetadataItem) {
return EMPTY_MUTATION;
return null;
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
@ -35,7 +37,7 @@ export const useGetRecordFromCache = ({
id: recordId,
});
return cache.readFragment({
return cache.readFragment<CachedObjectRecord>({
id: cachedRecordId,
fragment: cacheReadFragment,
});

View File

@ -1,8 +1,8 @@
import { useApolloClient } from '@apollo/client';
import { Modifier, Reference } from '@apollo/client/cache';
import { Modifiers } from '@apollo/client/cache';
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from '~/utils/string/capitalize';
export const useModifyRecordFromCache = ({
@ -10,23 +10,20 @@ export const useModifyRecordFromCache = ({
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const apolloClient = useApolloClient();
const { cache } = useApolloClient();
return (
return <CachedObjectRecord extends ObjectRecord>(
recordId: string,
fieldModifiers: Record<string, Modifier<Reference>>,
fieldModifiers: Modifiers<CachedObjectRecord>,
) => {
if (!objectMetadataItem) {
return EMPTY_MUTATION;
}
if (!objectMetadataItem) return;
const cache = apolloClient.cache;
const cachedRecordId = cache.identify({
__typename: capitalize(objectMetadataItem.nameSingular),
id: recordId,
});
cache.modify<Record<string, Reference>>({
cache.modify<CachedObjectRecord>({
id: cachedRecordId,
fields: fieldModifiers,
});

View File

@ -55,7 +55,7 @@ export const useObjectRecordBoard = () => {
useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.PipelineStep,
filter: {},
filter,
onCompleted: useCallback(
(data: ObjectRecordConnection<PipelineStep>) => {
setSavedPipelineSteps(data.edges.map((edge) => edge.node));

View File

@ -1,27 +1,23 @@
import { Reference, useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client';
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
import { capitalize } from '~/utils/string/capitalize';
type useUpdateOneRecordProps = {
objectNameSingular: string;
};
export const useUpdateOneRecord = <T>({
export const useUpdateOneRecord = <
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
}: useUpdateOneRecordProps) => {
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
useObjectMetadataItem({
objectNameSingular,
});
const { triggerOptimisticEffects } = useOptimisticEffect({
objectNameSingular,
});
useObjectMetadataItem({ objectNameSingular });
const apolloClient = useApolloClient();
@ -30,13 +26,15 @@ export const useUpdateOneRecord = <T>({
updateOneRecordInput,
}: {
idToUpdate: string;
updateOneRecordInput: Record<string, unknown>;
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
}) => {
const cachedRecord = getRecordFromCache(idToUpdate);
const cachedRecord = getRecordFromCache<UpdatedObjectRecord>(idToUpdate);
const optimisticallyUpdatedRecord: Record<string, any> = {
const optimisticallyUpdatedRecord = {
...(cachedRecord ?? {}),
...updateOneRecordInput,
__typename: capitalize(objectNameSingular),
id: idToUpdate,
};
const sanitizedUpdateOneRecordInput = sanitizeRecordInput({
@ -44,82 +42,32 @@ export const useUpdateOneRecord = <T>({
recordInput: updateOneRecordInput,
});
triggerOptimisticEffects({
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
updatedRecords: [optimisticallyUpdatedRecord],
});
const mutationResponseField =
getUpdateOneRecordMutationResponseField(objectNameSingular);
const updatedRecord = await apolloClient.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate,
input: {
...sanitizedUpdateOneRecordInput,
},
input: sanitizedUpdateOneRecordInput,
},
optimisticResponse: {
[`update${capitalize(objectMetadataItem.nameSingular)}`]:
optimisticallyUpdatedRecord,
[mutationResponseField]: optimisticallyUpdatedRecord,
},
update: (cache, { data }) => {
const response =
data?.[`update${capitalize(objectMetadataItem.nameSingular)}`];
const record = data?.[mutationResponseField];
if (!response) return;
if (!record) return;
cache.modify<Record<string, Reference>>({
fields: {
[objectMetadataItem.namePlural]: (
existingConnectionRef,
{ readField, storeFieldName },
) => {
if (
readField('__typename', existingConnectionRef) !==
`${capitalize(objectMetadataItem.nameSingular)}Connection`
)
return existingConnectionRef;
const { variables } = parseApolloStoreFieldName(storeFieldName);
const edges = readField<{ node: Reference }[]>(
'edges',
existingConnectionRef,
);
if (
variables?.filter &&
!isRecordMatchingFilter({
record: response,
filter: variables.filter,
objectMetadataItem,
}) &&
edges?.length
) {
return {
...existingConnectionRef,
edges: edges.filter(
(edge) =>
readField('id', readField('node', edge)) !== response.id,
),
};
}
return existingConnectionRef;
},
},
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
record,
});
},
});
if (!updatedRecord?.data) {
return null;
}
const updatedData = updatedRecord.data[
`update${capitalize(objectMetadataItem.nameSingular)}`
] as T;
return updatedData;
return updatedRecord?.data?.[mutationResponseField] ?? null;
};
return {

View File

@ -4,7 +4,6 @@ import { useRecoilCallback } from 'recoil';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
import { Opportunity } from '@/pipeline/types/Opportunity';
import { useRemoveRecordBoardCardIdsInternal } from './useRemoveRecordBoardCardIdsInternal';
@ -12,10 +11,9 @@ export const useDeleteSelectedRecordBoardCardsInternal = () => {
const removeCardIds = useRemoveRecordBoardCardIdsInternal();
const apolloClient = useApolloClient();
const { deleteManyRecords: deleteManyOpportunities } =
useDeleteManyRecords<Opportunity>({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const { deleteManyRecords: deleteManyOpportunities } = useDeleteManyRecords({
objectNameSingular: CoreObjectNameSingular.Opportunity,
});
const { selectedCardIdsSelector } = useRecordBoardScopedStates();

View File

@ -10,6 +10,7 @@ import {
URLFilter,
UUIDFilter,
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { Field } from '~/generated/graphql';
@ -24,7 +25,7 @@ export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
export const turnObjectDropdownFilterIntoQueryFilter = (
rawUIFilters: ObjectDropdownFilter[],
fields: Pick<Field, 'id' | 'name'>[],
): ObjectRecordQueryFilter => {
): ObjectRecordQueryFilter | undefined => {
const objectRecordFilters: ObjectRecordQueryFilter[] = [];
for (const rawUIFilter of rawUIFilters) {
@ -134,13 +135,15 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
});
break;
case ViewFilterOperand.IsNot:
objectRecordFilters.push({
not: {
[correspondingField.name + 'Id']: {
in: parsedRecordIds,
} as UUIDFilter,
},
});
if (parsedRecordIds.length) {
objectRecordFilters.push({
not: {
[correspondingField.name + 'Id']: {
in: parsedRecordIds,
} as UUIDFilter,
},
});
}
break;
default:
throw new Error(
@ -257,5 +260,5 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
}
}
return { and: objectRecordFilters };
return andFilterVariables(objectRecordFilters);
};

View File

@ -1,10 +1,10 @@
import { useContext, useEffect } from 'react';
import { Reference } from '@apollo/client';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { LightIconButton, MenuItem } from 'tsup.ui.index';
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldDisplay } from '@/object-record/field/components/FieldDisplay';
@ -13,7 +13,6 @@ import { usePersistField } from '@/object-record/field/hooks/usePersistField';
import { entityFieldsFamilyState } from '@/object-record/field/states/entityFieldsFamilyState';
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useModifyRecordFromCache } from '@/object-record/hooks/useModifyRecordFromCache';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { IconDotsVertical, IconUnlink } from '@/ui/display/icon';
@ -71,14 +70,10 @@ export const RecordRelationFieldCardContent = ({
objectMetadataNameSingular,
} = fieldDefinition.metadata as FieldRelationMetadata;
const { objectMetadataItem } = useObjectMetadataItem({
const { modifyRecordFromCache } = useObjectMetadataItem({
objectNameSingular: objectMetadataNameSingular ?? '',
});
const modifyRecordFromCache = useModifyRecordFromCache({
objectMetadataItem,
});
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const {
labelIdentifierFieldMetadata: relationLabelIdentifierFieldMetadata,
@ -104,13 +99,13 @@ export const RecordRelationFieldCardContent = ({
const { closeDropdown, isDropdownOpen } = useDropdown(dropdownScopeId);
// TODO: temporary as ChipDisplay expect to find the entity in the entityFieldsFamilyState
const setEntityFields = useSetRecoilState(
const setRelationEntityFields = useSetRecoilState(
entityFieldsFamilyState(relationRecord.id),
);
useEffect(() => {
setEntityFields(relationRecord);
}, [relationRecord, setEntityFields]);
setRelationEntityFields(relationRecord);
}, [relationRecord, setRelationEntityFields]);
if (!FieldContextProvider) return null;
@ -137,15 +132,18 @@ export const RecordRelationFieldCardContent = ({
});
modifyRecordFromCache(entityId, {
[fieldName]: (relationRef, { readField }) => {
const edges = readField<{ node: Reference }[]>('edges', relationRef);
[fieldName]: (cachedRelationConnection, { readField }) => {
const edges = readField<CachedObjectRecordEdge[]>(
'edges',
cachedRelationConnection,
);
if (!edges) {
return relationRef;
return cachedRelationConnection;
}
return {
...relationRef,
...cachedRelationConnection,
edges: edges.filter(({ node }) => {
const id = readField('id', node);
return id !== relationRecord.id;

View File

@ -139,7 +139,6 @@ export const RecordRelationFieldCardSection = () => {
],
orderByField: 'createdAt',
selectedIds: relationRecordIds,
excludeEntityIds: relationRecordIds,
objectNameSingular: relationObjectMetadataNameSingular,
});

View File

@ -42,19 +42,6 @@ const response = {
};
const mocks = [
{
request: {
query,
variables: {
filterNameSingular: { and: [{}, { id: { in: ['1'] } }] },
orderByNameSingular: { createdAt: 'DescNullsLast' },
limitNameSingular: 60,
},
},
result: jest.fn(() => ({
data: response,
})),
},
{
request: {
query,
@ -72,8 +59,20 @@ const mocks = [
request: {
query,
variables: {
orderByNameSingular: { createdAt: 'DescNullsLast' },
limitNameSingular: 60,
filterNameSingular: { and: [{}, { not: { id: { in: ['1'] } } }] },
},
},
result: jest.fn(() => ({
data: response,
})),
},
{
request: {
query,
variables: {
limitNameSingular: 60,
filterNameSingular: { not: { id: { in: ['1'] } } },
orderByNameSingular: { createdAt: 'DescNullsLast' },
},
},

View File

@ -53,23 +53,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
if (!isNonEmptyArray(selectedIds)) return null;
const searchFilter =
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
return [
`filter${capitalize(nameSingular)}`,
{
and: [
{
...searchFilter,
},
{
id: {
in: selectedIds,
},
},
],
},
searchFilterPerMetadataItemNameSingular[nameSingular],
];
})
.filter(isDefined),

View File

@ -1,5 +1,4 @@
import { useQuery } from '@apollo/client';
import { isNonEmptyArray } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem';
@ -14,7 +13,7 @@ import {
import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { isDefined } from '~/utils/isDefined';
import { capitalize } from '~/utils/string/capitalize';
@ -58,35 +57,19 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
)
.map(({ id }) => id);
const searchFilter =
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
const excludedIdsUnion = [...selectedIds, ...excludedIds];
const excludedIdsFilter = excludedIdsUnion.length
? { not: { id: { in: excludedIdsUnion } } }
: undefined;
const noFilter =
!isNonEmptyArray(excludedIdsUnion) &&
isDeeplyEqual(searchFilter, {});
const searchFilters = [
searchFilterPerMetadataItemNameSingular[nameSingular],
excludedIdsFilter,
];
return [
`filter${capitalize(nameSingular)}`,
!noFilter
? {
and: [
{
...searchFilter,
},
isNonEmptyArray(excludedIdsUnion)
? {
not: {
id: {
in: [...selectedIds, ...excludedIds],
},
},
}
: {},
],
}
: {},
andFilterVariables(searchFilters),
];
})
.filter(isDefined),

View File

@ -5,7 +5,8 @@ import { OrderBy } from '@/object-metadata/types/OrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
import { isDefined } from '~/utils/isDefined';
import { andFilterVariables } from '@/object-record/utils/andFilterVariables';
import { orFilterVariables } from '@/object-record/utils/orFilterVariables';
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
@ -37,85 +38,62 @@ export const useRecordsForSelect = ({
];
const orderByField = getObjectOrderByField(sortOrder);
const selectedIdsFilter = { id: { in: selectedIds } };
const { loading: selectedRecordsLoading, records: selectedRecordsData } =
useFindManyRecords({
filter: {
id: {
in: selectedIds,
},
},
filter: selectedIdsFilter,
orderBy: orderByField,
objectNameSingular,
skip: !selectedIds.length,
});
const searchFilter = filters
.map(({ fieldNames, filter }) => {
if (!isNonEmptyString(filter)) {
return undefined;
}
const searchFilters = filters.map(({ fieldNames, filter }) => {
if (!isNonEmptyString(filter)) {
return undefined;
}
return {
or: fieldNames.map((fieldName) => {
const fieldNameParts = fieldName.split('.');
return orFilterVariables(
fieldNames.map((fieldName) => {
const [parentFieldName, subFieldName] = fieldName.split('.');
if (fieldNameParts.length > 1) {
// Composite field
return {
[fieldNameParts[0]]: {
[fieldNameParts[1]]: {
ilike: `%${filter}%`,
},
},
};
}
if (subFieldName) {
// Composite field
return {
[fieldName]: {
ilike: `%${filter}%`,
[parentFieldName]: {
[subFieldName]: {
ilike: `%${filter}%`,
},
},
};
}),
};
})
.filter(isDefined);
}
return {
[fieldName]: {
ilike: `%${filter}%`,
},
};
}),
);
});
const {
loading: filteredSelectedRecordsLoading,
records: filteredSelectedRecordsData,
} = useFindManyRecords({
filter: {
and: [
{
and: searchFilter,
},
{
id: {
in: selectedIds,
},
},
],
},
filter: andFilterVariables([...searchFilters, selectedIdsFilter]),
orderBy: orderByField,
objectNameSingular,
skip: !selectedIds.length,
});
const notFilterIds = [...selectedIds, ...excludeEntityIds];
const notFilter = notFilterIds.length
? { not: { id: { in: notFilterIds } } }
: undefined;
const { loading: recordsToSelectLoading, records: recordsToSelectData } =
useFindManyRecords({
filter: {
and: [
{
and: searchFilter,
},
{
not: {
id: {
in: [...selectedIds, ...excludeEntityIds],
},
},
},
],
},
filter: andFilterVariables([...searchFilters, notFilter]),
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
orderBy: orderByField,
objectNameSingular,

View File

@ -1,6 +1,6 @@
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export type ObjectRecordEdge<T extends ObjectRecord> = {
export type ObjectRecordEdge<T extends ObjectRecord = ObjectRecord> = {
__typename?: string;
node: T;
cursor: string;

View File

@ -0,0 +1,14 @@
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { isDefined } from '~/utils/isDefined';
export const andFilterVariables = (
filters: (ObjectRecordQueryFilter | undefined)[],
): ObjectRecordQueryFilter | undefined => {
const definedFilters = filters.filter(isDefined);
if (!definedFilters.length) return undefined;
return definedFilters.length === 1
? definedFilters[0]
: { and: definedFilters };
};

View File

@ -4,6 +4,10 @@ import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { capitalize } from '~/utils/string/capitalize';
export const getDeleteOneRecordMutationResponseField = (
objectNameSingular: string,
) => `delete${capitalize(objectNameSingular)}`;
export const generateDeleteOneRecordMutation = ({
objectMetadataItem,
}: {
@ -15,9 +19,13 @@ export const generateDeleteOneRecordMutation = ({
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const mutationResponseField = getDeleteOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
return gql`
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
delete${capitalizedObjectName}(id: $idToDelete) {
${mutationResponseField}(id: $idToDelete) {
id
}
}

View File

@ -0,0 +1,14 @@
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
import { isDefined } from '~/utils/isDefined';
export const orFilterVariables = (
filters: (ObjectRecordQueryFilter | undefined)[],
): ObjectRecordQueryFilter | undefined => {
const definedFilters = filters.filter(isDefined);
if (!definedFilters.length) return undefined;
return definedFilters.length === 1
? definedFilters[0]
: { or: definedFilters };
};