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:
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user