mirror of
https://github.com/twentyhq/twenty.git
synced 2024-10-04 21:07:21 +03:00
Feat/record optimistic effect (#3076)
* WIP * WIP * POC working on hard coded completedAt field * Finished isRecordMatchingFilter, mock of pg_graphql filtering mechanism * Fixed and cleaned * Unregister unused optimistic effects * Fix lint * Fixes from review --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
a5f28b4395
commit
687c9131f4
@ -6,20 +6,18 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
type Task = Pick<Activity, 'id' | 'completedAt'>;
|
||||
|
||||
export const useCompleteTask = (task: Task) => {
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
||||
objectNameSingular: 'activity',
|
||||
refetchFindManyQuery: true,
|
||||
});
|
||||
|
||||
const completeTask = useCallback(
|
||||
(value: boolean) => {
|
||||
async (value: boolean) => {
|
||||
const completedAt = value ? new Date().toISOString() : null;
|
||||
updateOneActivity?.({
|
||||
await updateOneActivity?.({
|
||||
idToUpdate: task.id,
|
||||
input: {
|
||||
completedAt,
|
||||
},
|
||||
forceRefetch: true,
|
||||
});
|
||||
},
|
||||
[task.id, updateOneActivity],
|
||||
|
@ -1,21 +1,20 @@
|
||||
import {
|
||||
ApolloCache,
|
||||
DocumentNode,
|
||||
OperationVariables,
|
||||
useApolloClient,
|
||||
} from '@apollo/client';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { computeOptimisticEffectKey } from '@/apollo/optimistic-effect/utils/computeOptimisticEffectKey';
|
||||
import {
|
||||
EMPTY_QUERY,
|
||||
useObjectMetadataItem,
|
||||
} from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
import { optimisticEffectState } from '../states/optimisticEffectState';
|
||||
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
|
||||
import {
|
||||
OptimisticEffect,
|
||||
OptimisticEffectWriter,
|
||||
} from '../types/internal/OptimisticEffect';
|
||||
import { OptimisticEffectDefinition } from '../types/OptimisticEffectDefinition';
|
||||
|
||||
export const useOptimisticEffect = ({
|
||||
@ -23,17 +22,41 @@ export const useOptimisticEffect = ({
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { findManyRecordsQuery } = useObjectMetadataItem({
|
||||
const { findManyRecordsQuery, objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const registerOptimisticEffect = useRecoilCallback(
|
||||
const unregisterOptimisticEffect = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
<T>({
|
||||
({
|
||||
variables,
|
||||
definition,
|
||||
}: {
|
||||
variables: OperationVariables;
|
||||
variables: ObjectRecordQueryVariables;
|
||||
definition: OptimisticEffectDefinition;
|
||||
}) => {
|
||||
const optimisticEffects = snapshot
|
||||
.getLoadable(optimisticEffectState)
|
||||
.getValue();
|
||||
|
||||
const computedKey = computeOptimisticEffectKey({
|
||||
variables,
|
||||
definition,
|
||||
});
|
||||
|
||||
const { [computedKey]: _, ...rest } = optimisticEffects;
|
||||
|
||||
set(optimisticEffectState, rest);
|
||||
},
|
||||
);
|
||||
|
||||
const registerOptimisticEffect = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
variables,
|
||||
definition,
|
||||
}: {
|
||||
variables: ObjectRecordQueryVariables;
|
||||
definition: OptimisticEffectDefinition;
|
||||
}) => {
|
||||
if (findManyRecordsQuery === EMPTY_QUERY) {
|
||||
@ -46,21 +69,14 @@ export const useOptimisticEffect = ({
|
||||
.getLoadable(optimisticEffectState)
|
||||
.getValue();
|
||||
|
||||
const optimisticEffectWriter = ({
|
||||
const optimisticEffectWriter: OptimisticEffectWriter = ({
|
||||
cache,
|
||||
newData,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
query,
|
||||
variables,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
newData: unknown;
|
||||
deletedRecordIds?: string[];
|
||||
variables: OperationVariables;
|
||||
query: DocumentNode;
|
||||
isUsingFlexibleBackend?: boolean;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
}) => {
|
||||
if (objectMetadataItem) {
|
||||
const existingData = cache.readQuery({
|
||||
@ -77,10 +93,11 @@ export const useOptimisticEffect = ({
|
||||
variables,
|
||||
data: {
|
||||
[objectMetadataItem.namePlural]: definition.resolver({
|
||||
currentData: (existingData as any)?.[
|
||||
currentCacheData: (existingData as any)?.[
|
||||
objectMetadataItem.namePlural
|
||||
],
|
||||
newData,
|
||||
updatedRecords,
|
||||
createdRecords,
|
||||
deletedRecordIds,
|
||||
variables,
|
||||
}),
|
||||
@ -91,7 +108,7 @@ export const useOptimisticEffect = ({
|
||||
}
|
||||
|
||||
const existingData = cache.readQuery({
|
||||
query,
|
||||
query: query ?? findManyRecordsQuery,
|
||||
variables,
|
||||
});
|
||||
|
||||
@ -100,26 +117,40 @@ export const useOptimisticEffect = ({
|
||||
}
|
||||
};
|
||||
|
||||
const computedKey = computeOptimisticEffectKey({
|
||||
variables,
|
||||
definition,
|
||||
});
|
||||
|
||||
const optimisticEffect = {
|
||||
key: definition.key,
|
||||
variables,
|
||||
typename: definition.typename,
|
||||
query: definition.query,
|
||||
writer: optimisticEffectWriter,
|
||||
objectMetadataItem: definition.objectMetadataItem,
|
||||
isUsingFlexibleBackend: definition.isUsingFlexibleBackend,
|
||||
} satisfies OptimisticEffect<T>;
|
||||
objectMetadataItem,
|
||||
} satisfies OptimisticEffect;
|
||||
|
||||
set(optimisticEffectState, {
|
||||
...optimisticEffects,
|
||||
[definition.key]: optimisticEffect,
|
||||
[computedKey]: optimisticEffect,
|
||||
});
|
||||
},
|
||||
[findManyRecordsQuery, objectNameSingular, objectMetadataItem],
|
||||
);
|
||||
|
||||
const triggerOptimisticEffects = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(typename: string, newData: unknown, deletedRecordIds?: string[]) => {
|
||||
({
|
||||
typename,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
}: {
|
||||
typename: string;
|
||||
createdRecords?: Record<string, unknown>[];
|
||||
updatedRecords?: Record<string, unknown>[];
|
||||
deletedRecordIds?: string[];
|
||||
}) => {
|
||||
const optimisticEffects = snapshot
|
||||
.getLoadable(optimisticEffectState)
|
||||
.getValue();
|
||||
@ -127,20 +158,26 @@ export const useOptimisticEffect = ({
|
||||
for (const optimisticEffect of Object.values(optimisticEffects)) {
|
||||
// We need to update the typename when createObject type differs from listObject types
|
||||
// It is the case for apiKey, where the creation route returns an ApiKeyToken type
|
||||
const formattedNewData = isNonEmptyArray(newData)
|
||||
? newData.map((data: any) => {
|
||||
const formattedCreatedRecords = isNonEmptyArray(createdRecords)
|
||||
? createdRecords.map((data: any) => {
|
||||
return { ...data, __typename: typename };
|
||||
})
|
||||
: newData;
|
||||
: [];
|
||||
|
||||
const formattedUpdatedRecords = isNonEmptyArray(updatedRecords)
|
||||
? updatedRecords.map((data: any) => {
|
||||
return { ...data, __typename: typename };
|
||||
})
|
||||
: [];
|
||||
|
||||
if (optimisticEffect.typename === typename) {
|
||||
optimisticEffect.writer({
|
||||
cache: apolloClient.cache,
|
||||
query: optimisticEffect.query ?? ({} as DocumentNode),
|
||||
newData: formattedNewData,
|
||||
query: optimisticEffect.query,
|
||||
createdRecords: formattedCreatedRecords,
|
||||
updatedRecords: formattedUpdatedRecords,
|
||||
deletedRecordIds,
|
||||
variables: optimisticEffect.variables,
|
||||
isUsingFlexibleBackend: optimisticEffect.isUsingFlexibleBackend,
|
||||
objectMetadataItem: optimisticEffect.objectMetadataItem,
|
||||
});
|
||||
}
|
||||
@ -152,5 +189,6 @@ export const useOptimisticEffect = ({
|
||||
return {
|
||||
registerOptimisticEffect,
|
||||
triggerOptimisticEffects,
|
||||
unregisterOptimisticEffect,
|
||||
};
|
||||
};
|
||||
|
@ -2,9 +2,7 @@ import { atom } from 'recoil';
|
||||
|
||||
import { OptimisticEffect } from '../types/internal/OptimisticEffect';
|
||||
|
||||
export const optimisticEffectState = atom<
|
||||
Record<string, OptimisticEffect<unknown>>
|
||||
>({
|
||||
export const optimisticEffectState = atom<Record<string, OptimisticEffect>>({
|
||||
key: 'optimisticEffectState',
|
||||
default: {},
|
||||
});
|
||||
|
@ -5,10 +5,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { OptimisticEffectResolver } from './OptimisticEffectResolver';
|
||||
|
||||
export type OptimisticEffectDefinition = {
|
||||
key: string;
|
||||
query?: DocumentNode;
|
||||
typename: string;
|
||||
resolver: OptimisticEffectResolver;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
isUsingFlexibleBackend?: boolean;
|
||||
};
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { OperationVariables } from '@apollo/client';
|
||||
|
||||
export type OptimisticEffectResolver = ({
|
||||
currentData,
|
||||
newData,
|
||||
currentCacheData,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
variables,
|
||||
}: {
|
||||
currentData: any; //TODO: Change when decommissioning v1
|
||||
newData: any; //TODO: Change when decommissioning v1
|
||||
currentCacheData: any; //TODO: Change when decommissioning v1
|
||||
createdRecords?: Record<string, unknown>[];
|
||||
updatedRecords?: Record<string, unknown>[];
|
||||
deletedRecordIds?: string[];
|
||||
variables: OperationVariables;
|
||||
}) => void;
|
||||
|
@ -1,28 +1,30 @@
|
||||
import { ApolloCache, DocumentNode, OperationVariables } from '@apollo/client';
|
||||
import { ApolloCache, DocumentNode } from '@apollo/client';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
type OptimisticEffectWriter<T> = ({
|
||||
export type OptimisticEffectWriter = ({
|
||||
cache,
|
||||
newData,
|
||||
variables,
|
||||
query,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
variables,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
cache: ApolloCache<T>;
|
||||
query: DocumentNode;
|
||||
newData: T;
|
||||
cache: ApolloCache<any>;
|
||||
query?: DocumentNode;
|
||||
createdRecords?: Record<string, unknown>[];
|
||||
updatedRecords?: Record<string, unknown>[];
|
||||
deletedRecordIds?: string[];
|
||||
variables: OperationVariables;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
isUsingFlexibleBackend?: boolean;
|
||||
variables: ObjectRecordQueryVariables;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => void;
|
||||
|
||||
export type OptimisticEffect<T> = {
|
||||
key: string;
|
||||
export type OptimisticEffect = {
|
||||
query?: DocumentNode;
|
||||
typename: string;
|
||||
variables: OperationVariables;
|
||||
writer: OptimisticEffectWriter<T>;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
isUsingFlexibleBackend?: boolean;
|
||||
variables: ObjectRecordQueryVariables;
|
||||
writer: OptimisticEffectWriter;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { OptimisticEffectDefinition } from '@/apollo/optimistic-effect/types/OptimisticEffectDefinition';
|
||||
import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables';
|
||||
|
||||
export const computeOptimisticEffectKey = ({
|
||||
variables,
|
||||
definition,
|
||||
}: {
|
||||
variables: ObjectRecordQueryVariables;
|
||||
definition: OptimisticEffectDefinition;
|
||||
}) => {
|
||||
const computedKey =
|
||||
(definition.objectMetadataItem?.namePlural ?? definition.typename) +
|
||||
'-' +
|
||||
JSON.stringify(variables);
|
||||
|
||||
return computedKey;
|
||||
};
|
@ -9,7 +9,6 @@ import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { mapFavorites } from '@/favorites/utils/mapFavorites';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { getRecordOptimisticEffectDefinition } from '@/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
@ -29,15 +28,13 @@ export const useFavorites = ({
|
||||
updateOneRecordMutation: updateOneFavoriteMutation,
|
||||
createOneRecordMutation: createOneFavoriteMutation,
|
||||
deleteOneRecordMutation: deleteOneFavoriteMutation,
|
||||
objectMetadataItem: favoriteObjectMetadataItem,
|
||||
} = useObjectMetadataItem({
|
||||
objectNameSingular: 'favorite',
|
||||
});
|
||||
|
||||
const { registerOptimisticEffect, triggerOptimisticEffects } =
|
||||
useOptimisticEffect({
|
||||
objectNameSingular: 'favorite',
|
||||
});
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular: 'favorite',
|
||||
});
|
||||
const { performOptimisticEvict } = useOptimisticEvict();
|
||||
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
@ -65,19 +62,8 @@ export const useFavorites = ({
|
||||
if (!isDeeplyEqual(favorites, queriedFavorites)) {
|
||||
set(favoritesState, queriedFavorites);
|
||||
}
|
||||
|
||||
if (!favoriteObjectMetadataItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerOptimisticEffect({
|
||||
variables: { filter: {}, orderBy: {} },
|
||||
definition: getRecordOptimisticEffectDefinition({
|
||||
objectMetadataItem: favoriteObjectMetadataItem,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[favoriteObjectMetadataItem, registerOptimisticEffect],
|
||||
[],
|
||||
),
|
||||
});
|
||||
|
||||
@ -102,7 +88,10 @@ export const useFavorites = ({
|
||||
},
|
||||
});
|
||||
|
||||
triggerOptimisticEffects(`FavoriteEdge`, result.data[`createFavorite`]);
|
||||
triggerOptimisticEffects({
|
||||
typename: `FavoriteEdge`,
|
||||
createdRecords: [result.data[`createFavorite`]],
|
||||
});
|
||||
|
||||
const createdFavorite = result?.data?.createFavorite;
|
||||
|
||||
|
@ -0,0 +1,57 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { getRecordOptimisticEffectDefinition } from '@/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const useRecordOptimisticEffect = ({
|
||||
objectMetadataItem,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
filter?: ObjectRecordQueryFilter;
|
||||
orderBy?: OrderByField;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { registerOptimisticEffect, unregisterOptimisticEffect } =
|
||||
useOptimisticEffect({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
registerOptimisticEffect({
|
||||
definition: getRecordOptimisticEffectDefinition({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
variables: {
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterOptimisticEffect({
|
||||
definition: getRecordOptimisticEffectDefinition({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
variables: {
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
};
|
||||
}, [
|
||||
registerOptimisticEffect,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
objectMetadataItem,
|
||||
unregisterOptimisticEffect,
|
||||
]);
|
||||
};
|
@ -1,68 +1,109 @@
|
||||
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 { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const getRecordOptimisticEffectDefinition = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) =>
|
||||
({
|
||||
key: `record-create-optimistic-effect-definition-${objectMetadataItem.nameSingular}`,
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
resolver: ({
|
||||
currentData,
|
||||
newData,
|
||||
deletedRecordIds,
|
||||
}: {
|
||||
currentData: unknown;
|
||||
newData: { id: string } & Record<string, any>;
|
||||
deletedRecordIds?: string[];
|
||||
}) => {
|
||||
const newRecordPaginatedCacheField = produce<
|
||||
PaginatedRecordTypeResults<any>
|
||||
>(currentData as PaginatedRecordTypeResults<any>, (draft) => {
|
||||
if (newData) {
|
||||
if (!draft) {
|
||||
return {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
edges: [{ node: newData, cursor: '' }],
|
||||
pageInfo: {
|
||||
endCursor: '',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}): OptimisticEffectDefinition => ({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
resolver: ({
|
||||
currentCacheData: currentData,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
deletedRecordIds,
|
||||
variables,
|
||||
}) => {
|
||||
const newRecordPaginatedCacheField = produce<
|
||||
PaginatedRecordTypeResults<any>
|
||||
>(currentData as PaginatedRecordTypeResults<any>, (draft) => {
|
||||
const existingDataIsEmpty = !draft || !draft.edges || !draft.edges[0];
|
||||
|
||||
const existingRecord = draft.edges.find(
|
||||
(edge) => edge.node.id === newData.id,
|
||||
);
|
||||
if (existingRecord) {
|
||||
existingRecord.node = newData;
|
||||
return;
|
||||
}
|
||||
|
||||
draft.edges.unshift({
|
||||
node: newData,
|
||||
cursor: '',
|
||||
if (isNonEmptyArray(createdRecords)) {
|
||||
if (existingDataIsEmpty) {
|
||||
return {
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
});
|
||||
}
|
||||
edges: createdRecords.map((createdRecord) => ({
|
||||
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 (deletedRecordIds) {
|
||||
draft.edges = draft.edges.filter(
|
||||
(edge) => !deletedRecordIds.includes(edge.node.id),
|
||||
);
|
||||
}
|
||||
});
|
||||
if (existingRecord) {
|
||||
existingRecord.node = createdRecord;
|
||||
continue;
|
||||
}
|
||||
|
||||
return newRecordPaginatedCacheField;
|
||||
},
|
||||
isUsingFlexibleBackend: true,
|
||||
objectMetadataItem,
|
||||
}) satisfies OptimisticEffectDefinition;
|
||||
draft.edges.unshift({
|
||||
node: createdRecord,
|
||||
cursor: '',
|
||||
__typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (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,
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMeta
|
||||
import { useGenerateEmptyRecord } from '@/object-record/hooks/useGenerateEmptyRecord';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useCreateManyRecords = <T>({
|
||||
export const useCreateManyRecords = <T extends Record<string, unknown>>({
|
||||
objectNameSingular,
|
||||
}: ObjectMetadataItemIdentifier) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
@ -32,10 +32,17 @@ export const useCreateManyRecords = <T>({
|
||||
}));
|
||||
|
||||
withIds.forEach((record) => {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
generateEmptyRecord({ id: record.id }),
|
||||
);
|
||||
const emptyRecord: Record<string, unknown> | undefined =
|
||||
generateEmptyRecord({
|
||||
id: record.id,
|
||||
});
|
||||
|
||||
if (emptyRecord) {
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords: [emptyRecord],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const createdObjects = await apolloClient.mutate({
|
||||
@ -59,11 +66,9 @@ export const useCreateManyRecords = <T>({
|
||||
`create${capitalize(objectMetadataItem.namePlural)}`
|
||||
] as T[]) ?? [];
|
||||
|
||||
createdRecords.forEach((record) => {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
record,
|
||||
);
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords,
|
||||
});
|
||||
|
||||
return createdRecords;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
@ -14,16 +13,16 @@ type useCreateOneRecordProps = {
|
||||
|
||||
export const useCreateOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
refetchFindManyQuery = false,
|
||||
}: useCreateOneRecordProps) => {
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem, createOneRecordMutation, findManyRecordsQuery } =
|
||||
useObjectMetadataItem({
|
||||
const { objectMetadataItem, createOneRecordMutation } = useObjectMetadataItem(
|
||||
{
|
||||
objectNameSingular,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: type this with a minimal type at least with Record<string, any>
|
||||
const apolloClient = useApolloClient();
|
||||
@ -35,16 +34,16 @@ export const useCreateOneRecord = <T>({
|
||||
const createOneRecord = async (input: Record<string, any>) => {
|
||||
const recordId = v4();
|
||||
|
||||
const generatedEmptyRecord = generateEmptyRecord({
|
||||
const generatedEmptyRecord = generateEmptyRecord<Record<string, unknown>>({
|
||||
id: recordId,
|
||||
...input,
|
||||
});
|
||||
|
||||
if (generatedEmptyRecord) {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
generatedEmptyRecord,
|
||||
);
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdRecords: [generatedEmptyRecord],
|
||||
});
|
||||
}
|
||||
|
||||
const createdObject = await apolloClient.mutate({
|
||||
@ -56,22 +55,12 @@ export const useCreateOneRecord = <T>({
|
||||
[`create${capitalize(objectMetadataItem.nameSingular)}`]:
|
||||
generateEmptyRecord({ id: recordId, ...input }),
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
if (!createdObject.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
createdObject.data[
|
||||
`create${capitalize(objectMetadataItem.nameSingular)}`
|
||||
],
|
||||
);
|
||||
|
||||
return createdObject.data[
|
||||
`create${capitalize(objectMetadataItem.nameSingular)}`
|
||||
] as T;
|
||||
|
@ -30,11 +30,10 @@ export const useDeleteOneRecord = <T>({
|
||||
|
||||
const deleteOneRecord = useCallback(
|
||||
async (idToDelete: string) => {
|
||||
triggerOptimisticEffects(
|
||||
`${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
undefined,
|
||||
[idToDelete],
|
||||
);
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
deletedRecordIds: [idToDelete],
|
||||
});
|
||||
|
||||
performOptimisticEvict(
|
||||
capitalize(objectMetadataItem.nameSingular),
|
||||
|
@ -4,13 +4,12 @@ import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
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 { getRecordOptimisticEffectDefinition } from '@/object-record/graphql/optimistic-effect-definition/getRecordOptimisticEffectDefinition';
|
||||
import { ObjectRecordFilter } from '@/object-record/types/ObjectRecordFilter';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
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';
|
||||
@ -37,7 +36,7 @@ export const useFindManyRecords = <
|
||||
onCompleted,
|
||||
skip,
|
||||
}: ObjectMetadataItemIdentifier & {
|
||||
filter?: ObjectRecordFilter;
|
||||
filter?: ObjectRecordQueryFilter;
|
||||
orderBy?: OrderByField;
|
||||
limit?: number;
|
||||
onCompleted?: (data: PaginatedRecordTypeResults<RecordType>) => void;
|
||||
@ -65,8 +64,11 @@ export const useFindManyRecords = <
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { registerOptimisticEffect } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
useRecordOptimisticEffect({
|
||||
objectMetadataItem,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
});
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
@ -82,19 +84,6 @@ export const useFindManyRecords = <
|
||||
orderBy: orderBy ?? {},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (objectMetadataItem) {
|
||||
registerOptimisticEffect({
|
||||
variables: {
|
||||
filter: filter ?? {},
|
||||
orderBy: orderBy ?? {},
|
||||
limit: limit,
|
||||
},
|
||||
definition: getRecordOptimisticEffectDefinition({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
onCompleted?.(data[objectMetadataItem.namePlural]);
|
||||
|
||||
if (data?.[objectMetadataItem.namePlural]) {
|
||||
|
@ -164,6 +164,6 @@ export const useGenerateEmptyRecord = ({
|
||||
};
|
||||
|
||||
return {
|
||||
generateEmptyRecord: generateEmptyRecord,
|
||||
generateEmptyRecord,
|
||||
};
|
||||
};
|
||||
|
@ -5,8 +5,8 @@ import { Company } from '@/companies/types/Company';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { useRecordBoardScopedStates } from '@/object-record/record-board/hooks/internal/useRecordBoardScopedStates';
|
||||
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
|
||||
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
|
||||
import { turnFiltersIntoObjectRecordFilters } from '@/object-record/utils/turnFiltersIntoWhereClause';
|
||||
import { Opportunity } from '@/pipeline/types/Opportunity';
|
||||
import { PipelineStep } from '@/pipeline/types/PipelineStep';
|
||||
|
||||
@ -43,7 +43,7 @@ export const useObjectRecordBoard = () => {
|
||||
savedPipelineStepsState,
|
||||
);
|
||||
|
||||
const filter = turnFiltersIntoObjectRecordFilters(
|
||||
const filter = turnObjectDropdownFilterIntoQueryFilter(
|
||||
boardFilters,
|
||||
foundObjectMetadataItem?.fields ?? [],
|
||||
);
|
||||
|
@ -4,10 +4,10 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter';
|
||||
import { useRecordTableScopedStates } from '@/object-record/record-table/hooks/internal/useRecordTableScopedStates';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { isRecordTableInitialLoadingState } from '@/object-record/record-table/states/isRecordTableInitialLoadingState';
|
||||
import { turnFiltersIntoObjectRecordFilters } from '@/object-record/utils/turnFiltersIntoWhereClause';
|
||||
import { signInBackgroundMockCompanies } from '@/sign-in-background-mock/constants/signInBackgroundMockCompanies';
|
||||
|
||||
import { useFindManyRecords } from './useFindManyRecords';
|
||||
@ -32,7 +32,7 @@ export const useObjectRecordTable = () => {
|
||||
const tableSorts = useRecoilValue(tableSortsState);
|
||||
const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState);
|
||||
|
||||
const requestFilters = turnFiltersIntoObjectRecordFilters(
|
||||
const requestFilters = turnObjectDropdownFilterIntoQueryFilter(
|
||||
tableFilters,
|
||||
foundObjectMetadataItem?.fields ?? [],
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { useOptimisticEffect } from '@/apollo/optimistic-effect/hooks/useOptimisticEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
@ -11,14 +11,13 @@ type useUpdateOneRecordProps = {
|
||||
|
||||
export const useUpdateOneRecord = <T>({
|
||||
objectNameSingular,
|
||||
refetchFindManyQuery = false,
|
||||
}: useUpdateOneRecordProps) => {
|
||||
const {
|
||||
objectMetadataItem,
|
||||
updateOneRecordMutation,
|
||||
getRecordFromCache,
|
||||
findManyRecordsQuery,
|
||||
} = useObjectMetadataItem({
|
||||
const { objectMetadataItem, updateOneRecordMutation, getRecordFromCache } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { triggerOptimisticEffects } = useOptimisticEffect({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
@ -34,6 +33,16 @@ export const useUpdateOneRecord = <T>({
|
||||
}) => {
|
||||
const cachedRecord = getRecordFromCache(idToUpdate);
|
||||
|
||||
triggerOptimisticEffects({
|
||||
typename: `${capitalize(objectMetadataItem.nameSingular)}Edge`,
|
||||
updatedRecords: [
|
||||
{
|
||||
...(cachedRecord ?? {}),
|
||||
...input,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const updatedRecord = await apolloClient.mutate({
|
||||
mutation: updateOneRecordMutation,
|
||||
variables: {
|
||||
@ -48,18 +57,17 @@ export const useUpdateOneRecord = <T>({
|
||||
...input,
|
||||
},
|
||||
},
|
||||
refetchQueries: refetchFindManyQuery
|
||||
? [getOperationName(findManyRecordsQuery) ?? '']
|
||||
: [],
|
||||
});
|
||||
|
||||
if (!updatedRecord?.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return updatedRecord.data[
|
||||
const updatedData = updatedRecord.data[
|
||||
`update${capitalize(objectMetadataItem.nameSingular)}`
|
||||
] as T;
|
||||
|
||||
return updatedData;
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -9,6 +9,11 @@ export type UUIDFilter = {
|
||||
is?: IsFilter;
|
||||
};
|
||||
|
||||
export type BooleanFilter = {
|
||||
eq?: boolean;
|
||||
is?: IsFilter;
|
||||
};
|
||||
|
||||
export type StringFilter = {
|
||||
eq?: string;
|
||||
gt?: string;
|
||||
@ -36,6 +41,11 @@ export type FloatFilter = {
|
||||
is?: IsFilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Always use a DateFilter in the variables of a query, and never directly in the query.
|
||||
*
|
||||
* Because pg_graphql only works with ISO strings if it is passed to variables.
|
||||
*/
|
||||
export type DateFilter = {
|
||||
eq?: string;
|
||||
gt?: string;
|
||||
@ -53,6 +63,7 @@ export type CurrencyFilter = {
|
||||
|
||||
export type URLFilter = {
|
||||
url?: StringFilter;
|
||||
label?: StringFilter;
|
||||
};
|
||||
|
||||
export type FullNameFilter = {
|
||||
@ -67,14 +78,27 @@ export type LeafFilter =
|
||||
| DateFilter
|
||||
| CurrencyFilter
|
||||
| URLFilter
|
||||
| FullNameFilter;
|
||||
| FullNameFilter
|
||||
| BooleanFilter;
|
||||
|
||||
export type ObjectRecordFilter =
|
||||
| {
|
||||
and?: ObjectRecordFilter[];
|
||||
or?: ObjectRecordFilter[];
|
||||
not?: ObjectRecordFilter;
|
||||
}
|
||||
| {
|
||||
[fieldName: string]: LeafFilter;
|
||||
};
|
||||
export type AndObjectRecordFilter = {
|
||||
and?: ObjectRecordQueryFilter[];
|
||||
};
|
||||
|
||||
export type OrObjectRecordFilter = {
|
||||
or?: ObjectRecordQueryFilter[] | ObjectRecordQueryFilter;
|
||||
};
|
||||
|
||||
export type NotObjectRecordFilter = {
|
||||
not?: ObjectRecordQueryFilter;
|
||||
};
|
||||
|
||||
export type LeafObjectRecordFilter = {
|
||||
[fieldName: string]: LeafFilter;
|
||||
};
|
||||
|
||||
export type ObjectRecordQueryFilter =
|
||||
| LeafObjectRecordFilter
|
||||
| AndObjectRecordFilter
|
||||
| OrObjectRecordFilter
|
||||
| NotObjectRecordFilter;
|
@ -0,0 +1,46 @@
|
||||
import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter';
|
||||
|
||||
describe('isMatchingBooleanFilter', () => {
|
||||
describe('eq', () => {
|
||||
it('value equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingBooleanFilter({ booleanFilter: { eq: true }, value: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not equal eq filter', () => {
|
||||
expect(
|
||||
isMatchingBooleanFilter({ booleanFilter: { eq: true }, value: false }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('value is NULL', () => {
|
||||
expect(
|
||||
isMatchingBooleanFilter({
|
||||
booleanFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL', () => {
|
||||
expect(
|
||||
isMatchingBooleanFilter({
|
||||
booleanFilter: { is: 'NOT_NULL' },
|
||||
value: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL and false', () => {
|
||||
expect(
|
||||
isMatchingBooleanFilter({
|
||||
booleanFilter: { is: 'NOT_NULL' },
|
||||
value: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import { BooleanFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const isMatchingBooleanFilter = ({
|
||||
booleanFilter,
|
||||
value,
|
||||
}: {
|
||||
booleanFilter: BooleanFilter;
|
||||
value: boolean;
|
||||
}) => {
|
||||
if (booleanFilter.eq !== undefined) {
|
||||
return value === booleanFilter.eq;
|
||||
} else if (booleanFilter.is !== undefined) {
|
||||
if (booleanFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected value for string filter : ${JSON.stringify(booleanFilter)}`,
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,161 @@
|
||||
import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter';
|
||||
|
||||
describe('isMatchingDateFilter', () => {
|
||||
const testDate = '2023-12-19T12:15:29.810Z';
|
||||
|
||||
describe('eq', () => {
|
||||
it('value equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({ dateFilter: { eq: testDate }, value: testDate }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not equal eq filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { eq: testDate },
|
||||
value: '2023-12-18T12:15:29.810Z',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neq', () => {
|
||||
it('value does not equal neq filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { neq: testDate },
|
||||
value: '2023-12-18T12:15:29.810Z',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value equals neq filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { neq: testDate },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in', () => {
|
||||
it('value is in the array', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { in: [testDate, '2023-12-20T12:15:29.810Z'] },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not in the array', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: {
|
||||
in: ['2023-12-20T12:15:29.810Z', '2023-12-21T12:15:29.810Z'],
|
||||
},
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('value is NULL', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { is: 'NOT_NULL' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gt', () => {
|
||||
it('value is greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { gt: '2023-12-18T12:15:29.810Z' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { gt: '2023-12-20T12:15:29.810Z' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gte', () => {
|
||||
it('value is greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { gte: testDate },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { gte: '2023-12-20T12:15:29.810Z' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lt', () => {
|
||||
it('value is less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { lt: '2023-12-20T12:15:29.810Z' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({ dateFilter: { lt: testDate }, value: testDate }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lte', () => {
|
||||
it('value is less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { lte: testDate },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingDateFilter({
|
||||
dateFilter: { lte: '2023-12-18T12:15:29.810Z' },
|
||||
value: testDate,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,47 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { DateFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const isMatchingDateFilter = ({
|
||||
dateFilter,
|
||||
value,
|
||||
}: {
|
||||
dateFilter: DateFilter;
|
||||
value: string;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case dateFilter.eq !== undefined: {
|
||||
return DateTime.fromISO(value).equals(DateTime.fromISO(dateFilter.eq));
|
||||
}
|
||||
case dateFilter.neq !== undefined: {
|
||||
return !DateTime.fromISO(value).equals(DateTime.fromISO(dateFilter.neq));
|
||||
}
|
||||
case dateFilter.in !== undefined: {
|
||||
return dateFilter.in.includes(value);
|
||||
}
|
||||
case dateFilter.is !== undefined: {
|
||||
if (dateFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
case dateFilter.gt !== undefined: {
|
||||
return DateTime.fromISO(value) > DateTime.fromISO(dateFilter.gt);
|
||||
}
|
||||
case dateFilter.gte !== undefined: {
|
||||
return DateTime.fromISO(value) >= DateTime.fromISO(dateFilter.gte);
|
||||
}
|
||||
case dateFilter.lt !== undefined: {
|
||||
return DateTime.fromISO(value) < DateTime.fromISO(dateFilter.lt);
|
||||
}
|
||||
case dateFilter.lte !== undefined: {
|
||||
return DateTime.fromISO(value) <= DateTime.fromISO(dateFilter.lte);
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for string filter : ${JSON.stringify(dateFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,118 @@
|
||||
import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter';
|
||||
|
||||
describe('isMatchingFloatFilter', () => {
|
||||
describe('eq', () => {
|
||||
it('value equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { eq: 10 }, value: 10 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not equal eq filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { eq: 10 }, value: 20 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neq', () => {
|
||||
it('value does not equal neq filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { neq: 10 }, value: 20 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value equals neq filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { neq: 10 }, value: 10 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gt', () => {
|
||||
it('value is greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { gt: 10 }, value: 20 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { gt: 20 }, value: 10 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gte', () => {
|
||||
it('value is greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { gte: 10 }, value: 10 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { gte: 20 }, value: 10 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lt', () => {
|
||||
it('value is less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { lt: 20 }, value: 10 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { lt: 10 }, value: 20 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lte', () => {
|
||||
it('value is less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { lte: 10 }, value: 10 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { lte: 10 }, value: 20 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in', () => {
|
||||
it('value is in the array', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { in: [10, 20, 30] }, value: 20 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not in the array', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { in: [10, 30, 40] }, value: 20 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('value is NULL', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({
|
||||
floatFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL', () => {
|
||||
expect(
|
||||
isMatchingFloatFilter({ floatFilter: { is: 'NOT_NULL' }, value: 10 }),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
import { FloatFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const isMatchingFloatFilter = ({
|
||||
floatFilter,
|
||||
value,
|
||||
}: {
|
||||
floatFilter: FloatFilter;
|
||||
value: number;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case floatFilter.eq !== undefined: {
|
||||
return value === floatFilter.eq;
|
||||
}
|
||||
case floatFilter.neq !== undefined: {
|
||||
return value !== floatFilter.neq;
|
||||
}
|
||||
case floatFilter.gt !== undefined: {
|
||||
return value > floatFilter.gt;
|
||||
}
|
||||
case floatFilter.gte !== undefined: {
|
||||
return value >= floatFilter.gte;
|
||||
}
|
||||
case floatFilter.lt !== undefined: {
|
||||
return value < floatFilter.lt;
|
||||
}
|
||||
case floatFilter.lte !== undefined: {
|
||||
return value <= floatFilter.lte;
|
||||
}
|
||||
case floatFilter.in !== undefined: {
|
||||
return floatFilter.in.includes(value);
|
||||
}
|
||||
case floatFilter.is !== undefined: {
|
||||
if (floatFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for float filter : ${JSON.stringify(floatFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,236 @@
|
||||
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
|
||||
|
||||
describe('isMatchingStringFilter', () => {
|
||||
describe('eq', () => {
|
||||
it('value equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { eq: 'test' }, value: 'test' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { eq: 'test' },
|
||||
value: 'other',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neq', () => {
|
||||
it('value does not equal neq filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { neq: 'test' },
|
||||
value: 'other',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value equals neq filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { neq: 'test' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('like', () => {
|
||||
it('value matches like pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { like: 'te%' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not match like pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { like: 'ab%' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ilike', () => {
|
||||
it('value matches ilike pattern case insensitively', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { ilike: 'TE%' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not match ilike pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { ilike: 'AB%' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in', () => {
|
||||
it('value is in the array', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { in: ['test', 'example'] },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not in the array', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { in: ['example', 'sample'] },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('value is NULL', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { is: 'NOT_NULL' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regex', () => {
|
||||
it('value matches regex pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { regex: '^test$' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not match regex pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { regex: '^test$' },
|
||||
value: 'testing',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('iregex', () => {
|
||||
it('value matches iregex pattern case insensitively', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { iregex: '^test$' },
|
||||
value: 'Test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not match iregex pattern', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { iregex: '^test$' },
|
||||
value: 'testing',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gt', () => {
|
||||
it('value is greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gt: 'a' }, value: 'b' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than gt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gt: 'b' }, value: 'a' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gte', () => {
|
||||
it('value is greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gte: 'a' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not greater than or equal to gte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { gte: 'b' }, value: 'a' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lt', () => {
|
||||
it('value is less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lt: 'b' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than lt filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lt: 'a' }, value: 'b' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lte', () => {
|
||||
it('value is less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'a' }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not less than or equal to lte filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({ stringFilter: { lte: 'a' }, value: 'b' }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startsWith', () => {
|
||||
it('value starts with the startsWith filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { startsWith: 'te' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not start with the startsWith filter', () => {
|
||||
expect(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: { startsWith: 'st' },
|
||||
value: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,72 @@
|
||||
import { StringFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const isMatchingStringFilter = ({
|
||||
stringFilter,
|
||||
value,
|
||||
}: {
|
||||
stringFilter: StringFilter;
|
||||
value: string;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case stringFilter.eq !== undefined: {
|
||||
return value === stringFilter.eq;
|
||||
}
|
||||
case stringFilter.neq !== undefined: {
|
||||
return value !== stringFilter.neq;
|
||||
}
|
||||
case stringFilter.like !== undefined: {
|
||||
const regexPattern = stringFilter.like.replace(/%/g, '.*');
|
||||
const regexCaseSensitive = new RegExp(`^${regexPattern}$`);
|
||||
|
||||
return regexCaseSensitive.test(value);
|
||||
}
|
||||
case stringFilter.ilike !== undefined: {
|
||||
const regexPattern = stringFilter.ilike.replace(/%/g, '.*');
|
||||
const regexCaseInsensitive = new RegExp(`^${regexPattern}$`, 'i');
|
||||
|
||||
return regexCaseInsensitive.test(value);
|
||||
}
|
||||
case stringFilter.in !== undefined: {
|
||||
return stringFilter.in.includes(value);
|
||||
}
|
||||
case stringFilter.is !== undefined: {
|
||||
if (stringFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
case stringFilter.regex !== undefined: {
|
||||
const regexPattern = stringFilter.regex;
|
||||
const regexCaseSensitive = new RegExp(regexPattern);
|
||||
|
||||
return regexCaseSensitive.test(value);
|
||||
}
|
||||
case stringFilter.iregex !== undefined: {
|
||||
const regexPattern = stringFilter.iregex;
|
||||
const regexCaseInsensitive = new RegExp(regexPattern, 'i');
|
||||
|
||||
return regexCaseInsensitive.test(value);
|
||||
}
|
||||
case stringFilter.gt !== undefined: {
|
||||
return value > stringFilter.gt;
|
||||
}
|
||||
case stringFilter.gte !== undefined: {
|
||||
return value >= stringFilter.gte;
|
||||
}
|
||||
case stringFilter.lt !== undefined: {
|
||||
return value < stringFilter.lt;
|
||||
}
|
||||
case stringFilter.lte !== undefined: {
|
||||
return value <= stringFilter.lte;
|
||||
}
|
||||
case stringFilter.startsWith !== undefined: {
|
||||
return value.startsWith(stringFilter.startsWith);
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for string filter : ${JSON.stringify(stringFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,82 @@
|
||||
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
|
||||
|
||||
describe('isMatchingUUIDFilter', () => {
|
||||
const testUUID = '123e4567-e89b-12d3-a456-426655440000';
|
||||
|
||||
describe('eq', () => {
|
||||
it('value equals eq filter', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({ uuidFilter: { eq: testUUID }, value: testUUID }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value does not equal eq filter', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { eq: testUUID },
|
||||
value: 'different-uuid',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('neq', () => {
|
||||
it('value does not equal neq filter', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { neq: testUUID },
|
||||
value: 'different-uuid',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value equals neq filter', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { neq: testUUID },
|
||||
value: testUUID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in', () => {
|
||||
it('value is in the array', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { in: [testUUID, 'another-uuid'] },
|
||||
value: testUUID,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is not in the array', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { in: ['another-uuid', 'yet-another-uuid'] },
|
||||
value: testUUID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is', () => {
|
||||
it('value is NULL', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { is: 'NULL' },
|
||||
value: null as any,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('value is NOT_NULL', () => {
|
||||
expect(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter: { is: 'NOT_NULL' },
|
||||
value: testUUID,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,36 @@
|
||||
import {
|
||||
UUIDFilter,
|
||||
UUIDFilterValue,
|
||||
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export const isMatchingUUIDFilter = ({
|
||||
uuidFilter,
|
||||
value,
|
||||
}: {
|
||||
uuidFilter: UUIDFilter;
|
||||
value: UUIDFilterValue;
|
||||
}) => {
|
||||
switch (true) {
|
||||
case uuidFilter.eq !== undefined: {
|
||||
return value === uuidFilter.eq;
|
||||
}
|
||||
case uuidFilter.neq !== undefined: {
|
||||
return value !== uuidFilter.neq;
|
||||
}
|
||||
case uuidFilter.in !== undefined: {
|
||||
return uuidFilter.in.includes(value);
|
||||
}
|
||||
case uuidFilter.is !== undefined: {
|
||||
if (uuidFilter.is === 'NULL') {
|
||||
return value === null;
|
||||
} else {
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unexpected value for string filter : ${JSON.stringify(uuidFilter)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,344 @@
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
import { mockObjectMetadataItem } from '~/testing/mock-data/objectMetadataItems';
|
||||
|
||||
import { isRecordMatchingFilter } from './isRecordMatchingFilter';
|
||||
|
||||
describe('isRecordMatchingFilter', () => {
|
||||
describe('Empty Filters', () => {
|
||||
it('matches any record when no filter is provided', () => {
|
||||
const emptyFilter = {};
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter: emptyFilter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('matches any record when filter fields are empty', () => {
|
||||
const filterWithEmptyFields = {
|
||||
name: {},
|
||||
employees: {},
|
||||
};
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter: filterWithEmptyFields,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('matches any record with an empty and filter', () => {
|
||||
const filter = { and: [] };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('matches any record with an empty or filter', () => {
|
||||
const filter = { or: [] };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('matches any record with an empty not filter', () => {
|
||||
const filter = { not: {} };
|
||||
|
||||
mockedCompaniesData.forEach((company) => {
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: company,
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Simple Filters', () => {
|
||||
it('matches a record with a simple equality filter on name', () => {
|
||||
const filter = { name: { eq: 'Airbnb' } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a simple equality filter on domainName', () => {
|
||||
const filter = { domainName: { eq: 'airbnb.com' } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a greater than filter on employees', () => {
|
||||
const filter = { employees: { gt: 10 } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches a record with a boolean filter on idealCustomerProfile', () => {
|
||||
const filter = { idealCustomerProfile: { eq: true } };
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0],
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[4], // Assuming this record has idealCustomerProfile as false
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex And/Or/Not Nesting', () => {
|
||||
it('matches record with a combination of and + or filters', () => {
|
||||
const filter: ObjectRecordQueryFilter = {
|
||||
and: [
|
||||
{ domainName: { eq: 'airbnb.com' } },
|
||||
{
|
||||
or: [
|
||||
{ employees: { gt: 10 } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with nested not filter', () => {
|
||||
const filter: ObjectRecordQueryFilter = {
|
||||
not: {
|
||||
and: [
|
||||
{ name: { eq: 'Airbnb' } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false); // Should not match as it's Airbnb with idealCustomerProfile true
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[3], // Apple
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true); // Should match as it's not Airbnb
|
||||
});
|
||||
|
||||
it('matches record with deep nesting of and, or, and not filters', () => {
|
||||
const filter: ObjectRecordQueryFilter = {
|
||||
and: [
|
||||
{ domainName: { eq: 'apple.com' } },
|
||||
{
|
||||
or: [{ employees: { eq: 10 } }, { not: { name: { eq: 'Apple' } } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[3], // Apple
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[4], // Qonto
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with and filter at root level', () => {
|
||||
const filter: ObjectRecordQueryFilter = {
|
||||
and: [
|
||||
{ name: { eq: 'Facebook' } },
|
||||
{ idealCustomerProfile: { eq: true } },
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[5], // Facebook
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('matches record with or filter at root level including a not condition', () => {
|
||||
const filter: ObjectRecordQueryFilter = {
|
||||
or: [{ name: { eq: 'Sequoia' } }, { not: { employees: { eq: 1 } } }],
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[6], // Sequoia
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Implicit And Conditions', () => {
|
||||
it('matches record with implicit and of multiple operators within the same field', () => {
|
||||
const filter = {
|
||||
employees: { gt: 10, lt: 100000 },
|
||||
name: { eq: 'Airbnb' },
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true); // Matches as Airbnb's employee count is between 10 and 100000
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[1], // Aircall
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false); // Does not match as Aircall's employee count is not within the range
|
||||
});
|
||||
|
||||
it('matches record with implicit and within an object passed to or', () => {
|
||||
const filter = {
|
||||
or: {
|
||||
name: { eq: 'Airbnb' },
|
||||
domainName: { eq: 'airbnb.com' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[0], // Airbnb
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isRecordMatchingFilter({
|
||||
record: mockedCompaniesData[2], // Algolia
|
||||
filter,
|
||||
objectMetadataItem: mockObjectMetadataItem,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,269 @@
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
AndObjectRecordFilter,
|
||||
BooleanFilter,
|
||||
DateFilter,
|
||||
FloatFilter,
|
||||
FullNameFilter,
|
||||
LeafObjectRecordFilter,
|
||||
NotObjectRecordFilter,
|
||||
ObjectRecordQueryFilter,
|
||||
OrObjectRecordFilter,
|
||||
StringFilter,
|
||||
URLFilter,
|
||||
UUIDFilter,
|
||||
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter';
|
||||
import { isMatchingDateFilter } from '@/object-record/record-filter/utils/isMatchingDateFilter';
|
||||
import { isMatchingFloatFilter } from '@/object-record/record-filter/utils/isMatchingFloatFilter';
|
||||
import { isMatchingStringFilter } from '@/object-record/record-filter/utils/isMatchingStringFilter';
|
||||
import { isMatchingUUIDFilter } from '@/object-record/record-filter/utils/isMatchingUUIDFilter';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
|
||||
export const isRecordMatchingFilter = ({
|
||||
record,
|
||||
filter,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
record: any;
|
||||
filter: ObjectRecordQueryFilter;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
if (Object.keys(filter).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentLevelFilterMatches: boolean[] = [];
|
||||
|
||||
// We consider all the keys at the same level as an "and"
|
||||
for (const filterKey in filter) {
|
||||
if (filterKey === 'and') {
|
||||
const filterValue = (filter as AndObjectRecordFilter).and;
|
||||
|
||||
if (!Array.isArray(filterValue)) {
|
||||
throw new Error(
|
||||
'Unexpected value for "and" filter : ' + JSON.stringify(filterValue),
|
||||
);
|
||||
}
|
||||
|
||||
if (filterValue.length === 0) {
|
||||
currentLevelFilterMatches.push(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIsMatchingAndFilters = filterValue.every((andFilter) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: andFilter,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
|
||||
currentLevelFilterMatches.push(recordIsMatchingAndFilters);
|
||||
} else if (filterKey === 'or') {
|
||||
const filterValue = (filter as OrObjectRecordFilter).or;
|
||||
|
||||
if (Array.isArray(filterValue)) {
|
||||
if (filterValue.length === 0) {
|
||||
currentLevelFilterMatches.push(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIsMatchingOrFilters = filterValue.some((orFilter) =>
|
||||
isRecordMatchingFilter({
|
||||
record,
|
||||
filter: orFilter,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
);
|
||||
|
||||
currentLevelFilterMatches.push(recordIsMatchingOrFilters);
|
||||
} else if (isObject(filterValue)) {
|
||||
// The API considers "or" with an object as an "and"
|
||||
const recordIsMatchingOrFilters = isRecordMatchingFilter({
|
||||
record,
|
||||
filter: filterValue,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
currentLevelFilterMatches.push(recordIsMatchingOrFilters);
|
||||
} else {
|
||||
throw new Error('Unexpected value for "or" filter : ' + filterValue);
|
||||
}
|
||||
} else if (filterKey === 'not') {
|
||||
const filterValue = (filter as NotObjectRecordFilter).not;
|
||||
|
||||
if (!isDefined(filterValue)) {
|
||||
throw new Error('Unexpected value for "not" filter : ' + filterValue);
|
||||
}
|
||||
|
||||
if (isEmptyObject(filterValue)) {
|
||||
currentLevelFilterMatches.push(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIsMatchingNotFilters = !isRecordMatchingFilter({
|
||||
record,
|
||||
filter: filterValue,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
currentLevelFilterMatches.push(recordIsMatchingNotFilters);
|
||||
} else {
|
||||
const filterValue = (filter as LeafObjectRecordFilter)[filterKey];
|
||||
|
||||
if (!isDefined(filterValue)) {
|
||||
throw new Error(
|
||||
'Unexpected value for filter key "' +
|
||||
filterKey +
|
||||
'" : ' +
|
||||
filterValue,
|
||||
);
|
||||
}
|
||||
|
||||
if (isEmptyObject(filterValue)) {
|
||||
currentLevelFilterMatches.push(true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectMetadataField = objectMetadataItem.fields.find(
|
||||
(field) => field.name === filterKey,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataField)) {
|
||||
throw new Error(
|
||||
'Field metadata item "' +
|
||||
filterKey +
|
||||
'" not found for object metadata item ' +
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
}
|
||||
|
||||
switch (objectMetadataField.type) {
|
||||
case FieldMetadataType.Email:
|
||||
case FieldMetadataType.Phone:
|
||||
case FieldMetadataType.Text: {
|
||||
const stringFilter = filterValue as StringFilter;
|
||||
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingStringFilter({
|
||||
stringFilter,
|
||||
value: record[filterKey],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.Link: {
|
||||
const urlFilter = filterValue as URLFilter;
|
||||
|
||||
if (urlFilter.url !== undefined) {
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: urlFilter.url,
|
||||
value: record[filterKey].url,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (urlFilter.label !== undefined) {
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: urlFilter.label,
|
||||
value: record[filterKey].label,
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.FullName: {
|
||||
const fullNameFilter = filterValue as FullNameFilter;
|
||||
|
||||
if (fullNameFilter.firstName !== undefined) {
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: fullNameFilter.firstName,
|
||||
value: record[filterKey].firstName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (fullNameFilter.lastName !== undefined) {
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingStringFilter({
|
||||
stringFilter: fullNameFilter.lastName,
|
||||
value: record[filterKey].lastName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.DateTime: {
|
||||
const dateFilter = filterValue as DateFilter;
|
||||
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingDateFilter({
|
||||
dateFilter,
|
||||
value: record[filterKey],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.Number:
|
||||
case FieldMetadataType.Numeric: {
|
||||
const numberFilter = filterValue as FloatFilter;
|
||||
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingFloatFilter({
|
||||
floatFilter: numberFilter,
|
||||
value: record[filterKey],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.Uuid: {
|
||||
const uuidFilter = filterValue as UUIDFilter;
|
||||
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingUUIDFilter({
|
||||
uuidFilter,
|
||||
value: record[filterKey],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.Boolean: {
|
||||
const booleanFilter = filterValue as BooleanFilter;
|
||||
|
||||
currentLevelFilterMatches.push(
|
||||
isMatchingBooleanFilter({
|
||||
booleanFilter,
|
||||
value: record[filterKey],
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FieldMetadataType.Relation: {
|
||||
throw new Error(
|
||||
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
||||
);
|
||||
}
|
||||
case FieldMetadataType.Currency:
|
||||
case FieldMetadataType.MultiSelect:
|
||||
case FieldMetadataType.Select:
|
||||
case FieldMetadataType.Probability:
|
||||
case FieldMetadataType.Rating: {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentLevelFilterMatches.length > 0
|
||||
? currentLevelFilterMatches.every((match) => !!match)
|
||||
: false;
|
||||
};
|
@ -1,28 +1,31 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import {
|
||||
CurrencyFilter,
|
||||
DateFilter,
|
||||
FloatFilter,
|
||||
FullNameFilter,
|
||||
ObjectRecordFilter,
|
||||
ObjectRecordQueryFilter,
|
||||
StringFilter,
|
||||
URLFilter,
|
||||
} from '@/object-record/types/ObjectRecordFilter';
|
||||
UUIDFilter,
|
||||
} from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { Field } from '~/generated/graphql';
|
||||
|
||||
import { Filter } from '../object-filter-dropdown/types/Filter';
|
||||
import { Filter } from '../../object-filter-dropdown/types/Filter';
|
||||
|
||||
export type RawUIFilter = Omit<Filter, 'definition'> & {
|
||||
export type ObjectDropdownFilter = Omit<Filter, 'definition'> & {
|
||||
definition: {
|
||||
type: Filter['definition']['type'];
|
||||
};
|
||||
};
|
||||
|
||||
export const turnFiltersIntoObjectRecordFilters = (
|
||||
rawUIFilters: RawUIFilter[],
|
||||
export const turnObjectDropdownFilterIntoQueryFilter = (
|
||||
rawUIFilters: ObjectDropdownFilter[],
|
||||
fields: Pick<Field, 'id' | 'name'>[],
|
||||
): ObjectRecordFilter => {
|
||||
const objectRecordFilters: ObjectRecordFilter[] = [];
|
||||
): ObjectRecordQueryFilter => {
|
||||
const objectRecordFilters: ObjectRecordQueryFilter[] = [];
|
||||
|
||||
for (const rawUIFilter of rawUIFilters) {
|
||||
const correspondingField = fields.find(
|
||||
@ -107,6 +110,10 @@ export const turnFiltersIntoObjectRecordFilters = (
|
||||
}
|
||||
break;
|
||||
case 'RELATION': {
|
||||
if (!isNonEmptyString(rawUIFilter.value)) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(rawUIFilter.value);
|
||||
} catch (e) {
|
||||
@ -123,7 +130,7 @@ export const turnFiltersIntoObjectRecordFilters = (
|
||||
objectRecordFilters.push({
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as StringFilter,
|
||||
} as UUIDFilter,
|
||||
});
|
||||
break;
|
||||
case ViewFilterOperand.IsNot:
|
||||
@ -131,7 +138,7 @@ export const turnFiltersIntoObjectRecordFilters = (
|
||||
not: {
|
||||
[correspondingField.name + 'Id']: {
|
||||
in: parsedRecordIds,
|
||||
} as StringFilter,
|
||||
} as UUIDFilter,
|
||||
},
|
||||
});
|
||||
break;
|
@ -0,0 +1,8 @@
|
||||
import { OrderByField } from '@/object-metadata/types/OrderByField';
|
||||
import { ObjectRecordQueryFilter } from '@/object-record/record-filter/types/ObjectRecordQueryFilter';
|
||||
|
||||
export type ObjectRecordQueryVariables = {
|
||||
filter?: ObjectRecordQueryFilter;
|
||||
orderBy?: OrderByField;
|
||||
limit?: number;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
@ -53,9 +54,11 @@ export const ViewBarFilterEffect = ({
|
||||
filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||
);
|
||||
|
||||
const viewFilterSelectedRecordIds = JSON.parse(
|
||||
viewFilterUsedInDropdown?.value ?? '[]',
|
||||
);
|
||||
const viewFilterSelectedRecordIds = isNonEmptyString(
|
||||
viewFilterUsedInDropdown?.value,
|
||||
)
|
||||
? JSON.parse(viewFilterUsedInDropdown.value)
|
||||
: [];
|
||||
|
||||
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds);
|
||||
}
|
||||
|
@ -0,0 +1,417 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { FieldMetadataType, RelationMetadataType } from '~/generated/graphql';
|
||||
|
||||
export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
__typename: 'object',
|
||||
id: 'b79a038c-b06b-4a5a-b7ee-f8ba412aa1c0',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: 'A company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
imageIdentifierFieldMetadataId: null,
|
||||
fields: [
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '390eb5e5-d8d1-4064-bf75-3461251eb142',
|
||||
type: FieldMetadataType.Boolean,
|
||||
name: 'idealCustomerProfile',
|
||||
label: 'ICP',
|
||||
description:
|
||||
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
|
||||
icon: 'IconTarget',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '72a43010-f236-4fa2-8ac4-a31e6b37d692',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'people',
|
||||
label: 'People',
|
||||
description: 'People linked to the company.',
|
||||
icon: 'IconUsers',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: {
|
||||
id: 'f08943fe-e8a0-4747-951c-c3b391842453',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
toObjectMetadata: {
|
||||
id: 'fcccc985-5edf-405c-aa2b-80c82b230f35',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
},
|
||||
toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '51636fba-1bd9-4344-bba8-9639cbc8e134',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
description: 'Opportunities linked to the company.',
|
||||
icon: 'IconTargetArrow',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: {
|
||||
id: '7ffae8bb-b12b-4ad9-8922-da0d517b5612',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
toObjectMetadata: {
|
||||
id: '169e5b21-dc95-44a8-acd0-5e9447dd0784',
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
},
|
||||
toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'd541f76b-d327-4dda-8ef8-81b60e5ad01e',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'activityTargets',
|
||||
label: 'Activities',
|
||||
description: 'Activities tied to the company',
|
||||
icon: 'IconCheckbox',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: {
|
||||
id: 'bc42672b-350f-45c3-bd1f-4debb536ccd1',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
toObjectMetadata: {
|
||||
id: 'b87c6cac-a8e7-4156-a525-30ec536acd75',
|
||||
nameSingular: 'activityTarget',
|
||||
namePlural: 'activityTargets',
|
||||
},
|
||||
toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'dacb7562-497e-4080-8ef5-746d6786ed49',
|
||||
type: FieldMetadataType.DateTime,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: null,
|
||||
icon: 'IconCalendar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
type: 'now',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'f3b4ff22-800b-4f13-8262-8003da8eed5b',
|
||||
type: FieldMetadataType.Number,
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
description: 'Number of employees in the company',
|
||||
icon: 'IconUsers',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'c3e64012-32cc-43f1-af2f-33b37cc4e59d',
|
||||
type: FieldMetadataType.Link,
|
||||
name: 'linkedinLink',
|
||||
label: 'Linkedin',
|
||||
description: 'The company Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'fced9acc-0374-487d-9da4-579a17435df0',
|
||||
type: FieldMetadataType.Link,
|
||||
name: 'xLink',
|
||||
label: 'X',
|
||||
description: 'The company Twitter/X account',
|
||||
icon: 'IconBrandX',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '63db0a2f-ffb4-4ea1-98c7-f7e13ce75c38',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
description: 'Attachments linked to the company.',
|
||||
icon: 'IconFileImport',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: {
|
||||
id: '901fd405-c6bf-4559-9d1f-d0937b6f16d9',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
toObjectMetadata: {
|
||||
id: '77240b4b-6bcf-454d-a102-19bbba181716',
|
||||
nameSingular: 'attachment',
|
||||
namePlural: 'attachments',
|
||||
},
|
||||
toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'e775ce12-87c0-4feb-bcfe-9af3d8ca117b',
|
||||
type: FieldMetadataType.Uuid,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: null,
|
||||
icon: null,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
type: 'uuid',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '2278ef91-3d6a-45cf-86f5-76b7bfa2bf32',
|
||||
type: FieldMetadataType.Text,
|
||||
name: 'domainName',
|
||||
label: 'Domain Name',
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '438291d7-18f4-48cf-8dca-05e96c5a0765',
|
||||
type: FieldMetadataType.Currency,
|
||||
name: 'annualRecurringRevenue',
|
||||
label: 'ARR',
|
||||
description:
|
||||
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
|
||||
icon: 'IconMoneybag',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'edb8475f-03fc-4ac1-9305-e9d4e2dacd11',
|
||||
type: FieldMetadataType.DateTime,
|
||||
name: 'updatedAt',
|
||||
label: 'Update date',
|
||||
description: null,
|
||||
icon: 'IconCalendar',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: false,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
type: 'now',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'e3c9ba7f-cecf-4ac6-a7b9-7a9987be0253',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'accountOwner',
|
||||
label: 'Account Owner',
|
||||
description:
|
||||
'Your team member responsible for managing the company account',
|
||||
icon: 'IconUserCircle',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: {
|
||||
id: '0317d74c-5187-491f-9e1d-d22f06ca2a38',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
fromObjectMetadata: {
|
||||
id: '92c306ce-ad06-4712-99d2-5d0daf13c95f',
|
||||
nameSingular: 'workspaceMember',
|
||||
namePlural: 'workspaceMembers',
|
||||
},
|
||||
fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159',
|
||||
},
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'a34bd3b3-6949-4793-bac6-d2c054639c7f',
|
||||
type: FieldMetadataType.Text,
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
description: 'The company address',
|
||||
icon: 'IconMap',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '4b204845-f1fc-4fd8-8fdd-f4caeaab749f',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
description: 'Favorites linked to the company',
|
||||
icon: 'IconHeart',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: {
|
||||
id: '8e0d3aa1-6135-4d65-aa28-15a5b6d1619c',
|
||||
relationType: RelationMetadataType.OneToMany,
|
||||
toObjectMetadata: {
|
||||
id: '1415392e-0ecb-462e-aa67-001e424e6a37',
|
||||
nameSingular: 'favorite',
|
||||
namePlural: 'favorites',
|
||||
},
|
||||
toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1',
|
||||
},
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: 'a795e81e-0bcf-4fd6-8f2f-b3764b990d2d',
|
||||
type: FieldMetadataType.Uuid,
|
||||
name: 'accountOwnerId',
|
||||
label: 'Account Owner id (foreign key)',
|
||||
description:
|
||||
'Your team member responsible for managing the company account id foreign key',
|
||||
icon: 'IconUserCircle',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
isNullable: true,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: null,
|
||||
},
|
||||
{
|
||||
__typename: 'field',
|
||||
id: '87887d23-f632-4d3e-840a-02fcee960660',
|
||||
type: FieldMetadataType.Text,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'The company name',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isNullable: false,
|
||||
createdAt: '2023-12-19T12:15:28.459Z',
|
||||
updatedAt: '2023-12-19T12:15:28.459Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
defaultValue: {
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
3
packages/twenty-front/src/utils/isAnObject.ts
Normal file
3
packages/twenty-front/src/utils/isAnObject.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const isAnObject = (obj: any): obj is object => {
|
||||
return typeof obj === 'object' && obj !== null && Object.keys(obj).length > 0;
|
||||
};
|
5
packages/twenty-front/src/utils/isEmptyObject.ts
Normal file
5
packages/twenty-front/src/utils/isEmptyObject.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
export const isEmptyObject = (obj: any): obj is object => {
|
||||
return isObject(obj) && Object.keys(obj).length === 0;
|
||||
};
|
Loading…
Reference in New Issue
Block a user