Support define is tool logic function (#17926)

- supports isTool and timeout settings in defineLogicFunction in apps
and in setting tabs definition
- compute for all toolInputSchema for logic funciton, in settings and in
code steps

<img width="991" height="802" alt="image"
src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd"
/>
This commit is contained in:
martmull
2026-02-16 10:43:29 +01:00
committed by GitHub
parent f694bb99b3
commit da064d5e88
138 changed files with 4839 additions and 367 deletions
@@ -0,0 +1,44 @@
import { fromArrayToValuesByKeyRecord } from '@/utils/fromArrayToValuesByKeyRecord.util';
describe('fromArrayToValuesByKeyRecord', () => {
it('should group items by the specified key', () => {
const array = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'admin' },
];
const result = fromArrayToValuesByKeyRecord({ array, key: 'role' });
expect(result).toEqual({
admin: [
{ name: 'Alice', role: 'admin' },
{ name: 'Charlie', role: 'admin' },
],
user: [{ name: 'Bob', role: 'user' }],
});
});
it('should return an empty object for an empty array', () => {
const result = fromArrayToValuesByKeyRecord({
array: [] as { name: string }[],
key: 'name',
});
expect(result).toEqual({});
});
it('should handle unique keys', () => {
const array = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
];
const result = fromArrayToValuesByKeyRecord({ array, key: 'name' });
expect(result).toEqual({
Alice: [{ name: 'Alice', role: 'admin' }],
Bob: [{ name: 'Bob', role: 'user' }],
});
});
});
@@ -0,0 +1,16 @@
import { getURLSafely } from '@/utils/getURLSafely';
describe('getURLSafely', () => {
it('should return a URL object for a valid URL', () => {
const result = getURLSafely('https://example.com');
expect(result).toBeInstanceOf(URL);
expect(result?.hostname).toBe('example.com');
});
it('should return null for an invalid URL', () => {
const result = getURLSafely('not-a-url');
expect(result).toBeNull();
});
});
@@ -0,0 +1,36 @@
import { removePropertiesFromRecord } from '@/utils/removePropertiesFromRecord';
describe('removePropertiesFromRecord', () => {
it('should remove specified keys from the record', () => {
const record = { a: 1, b: 2, c: 3 };
const result = removePropertiesFromRecord(record, ['b']);
expect(result).toEqual({ a: 1, c: 3 });
expect('b' in result).toBe(false);
});
it('should remove multiple keys', () => {
const record = { a: 1, b: 2, c: 3 };
const result = removePropertiesFromRecord(record, ['a', 'c']);
expect(result).toEqual({ b: 2 });
});
it('should not mutate the original record', () => {
const record = { a: 1, b: 2 };
removePropertiesFromRecord(record, ['a']);
expect(record).toEqual({ a: 1, b: 2 });
});
it('should return the same shape when no keys are removed', () => {
const record = { a: 1, b: 2 };
const result = removePropertiesFromRecord(record, []);
expect(result).toEqual({ a: 1, b: 2 });
});
});
@@ -0,0 +1,25 @@
import { uuidToBase36 } from '@/utils/uuidToBase36';
describe('uuidToBase36', () => {
it('should convert a UUID to base36', () => {
const uuid = '550e8400-e29b-41d4-a716-446655440000';
const result = uuidToBase36(uuid);
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
// base36 only contains alphanumeric characters
expect(result).toMatch(/^[0-9a-z]+$/);
});
it('should produce consistent results', () => {
const uuid = '00000000-0000-0000-0000-000000000001';
expect(uuidToBase36(uuid)).toBe(uuidToBase36(uuid));
});
it('should convert the zero UUID', () => {
const uuid = '00000000-0000-0000-0000-000000000000';
expect(uuidToBase36(uuid)).toBe('0');
});
});
@@ -0,0 +1,40 @@
import { filterOutByProperty } from '@/utils/array/filterOutByProperty';
describe('filterOutByProperty', () => {
it('should filter out items matching the excluded value', () => {
const items = [
{ id: '1', status: 'active' },
{ id: '2', status: 'inactive' },
{ id: '3', status: 'active' },
];
const result = items.filter(filterOutByProperty('status', 'inactive'));
expect(result).toEqual([
{ id: '1', status: 'active' },
{ id: '3', status: 'active' },
]);
});
it('should keep all items when no match exists', () => {
const items = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
];
const result = items.filter(filterOutByProperty('name', 'Charlie'));
expect(result).toEqual(items);
});
it('should handle null exclusion value', () => {
const items = [
{ id: '1', name: null as string | null },
{ id: '2', name: 'Bob' },
];
const result = items.filter(filterOutByProperty('name', null));
expect(result).toEqual([{ id: '2', name: 'Bob' }]);
});
});
@@ -0,0 +1,33 @@
import { findByProperty } from '@/utils/array/findByProperty';
describe('findByProperty', () => {
it('should find item matching the value', () => {
const items = [
{ id: '1', status: 'active' },
{ id: '2', status: 'inactive' },
];
const result = items.find(findByProperty('status', 'inactive'));
expect(result).toEqual({ id: '2', status: 'inactive' });
});
it('should return undefined when no match exists', () => {
const items = [{ id: '1', name: 'Alice' }];
const result = items.find(findByProperty('name', 'Bob'));
expect(result).toBeUndefined();
});
it('should handle null match value', () => {
const items = [
{ id: '1', name: null as string | null },
{ id: '2', name: 'Bob' },
];
const result = items.find(findByProperty('name', null));
expect(result).toEqual({ id: '1', name: null });
});
});
@@ -0,0 +1,19 @@
import { getContiguousIncrementalValues } from '@/utils/array/getContiguousIncrementalValues';
describe('getContiguousIncrementalValues', () => {
it('should return incremental values starting from 0 by default', () => {
expect(getContiguousIncrementalValues(5)).toEqual([0, 1, 2, 3, 4]);
});
it('should return incremental values starting from a custom value', () => {
expect(getContiguousIncrementalValues(3, 10)).toEqual([10, 11, 12]);
});
it('should return an empty array for 0 values', () => {
expect(getContiguousIncrementalValues(0)).toEqual([]);
});
it('should handle negative starting values', () => {
expect(getContiguousIncrementalValues(3, -2)).toEqual([-2, -1, 0]);
});
});
@@ -0,0 +1,13 @@
import { mapById } from '@/utils/array/mapById';
describe('mapById', () => {
it('should extract the id from an item', () => {
expect(mapById({ id: 'abc-123' })).toBe('abc-123');
});
it('should work with Array.map', () => {
const items = [{ id: '1' }, { id: '2' }, { id: '3' }];
expect(items.map(mapById)).toEqual(['1', '2', '3']);
});
});
@@ -0,0 +1,18 @@
import { mapByProperty } from '@/utils/array/mapByProperty';
describe('mapByProperty', () => {
it('should extract the specified property from an item', () => {
const items = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
];
expect(items.map(mapByProperty('name'))).toEqual(['Alice', 'Bob']);
});
it('should extract the id property', () => {
const items = [{ id: 'a' }, { id: 'b' }];
expect(items.map(mapByProperty('id'))).toEqual(['a', 'b']);
});
});
@@ -0,0 +1,29 @@
import { sumByProperty } from '@/utils/array/sumByProperty';
describe('sumByProperty', () => {
it('should sum numeric property values', () => {
const items = [
{ id: '1', amount: 10 },
{ id: '2', amount: 20 },
{ id: '3', amount: 30 },
];
expect(items.reduce(sumByProperty('amount'), 0)).toBe(60);
});
it('should skip non-numeric values', () => {
const items = [
{ id: '1', amount: 10 },
{ id: '2', amount: 'not-a-number' as unknown as number },
{ id: '3', amount: 30 },
];
expect(items.reduce(sumByProperty('amount'), 0)).toBe(40);
});
it('should handle an empty array', () => {
const items: { id: string; amount: number }[] = [];
expect(items.reduce(sumByProperty('amount'), 0)).toBe(0);
});
});
@@ -0,0 +1,67 @@
import { Temporal } from 'temporal-polyfill';
import { parseToPlainDateOrThrow } from '@/utils/date/parseToPlainDateOrThrow';
import { turnJSDateToPlainDate } from '@/utils/date/turnJSDateToPlainDate';
import { turnPlainDateIntoUserTimeZoneInstantString } from '@/utils/date/turnPlainDateIntoUserTimeZoneInstantString';
import { turnPlainDateToShiftedDateInSystemTimeZone } from '@/utils/date/turnPlainDateToShiftedDateInSystemTimeZone';
describe('parseToPlainDateOrThrow', () => {
it('should parse an ISO instant string', () => {
const result = parseToPlainDateOrThrow('2024-03-15T10:00:00Z');
expect(result.year).toBe(2024);
expect(result.month).toBe(3);
expect(result.day).toBe(15);
});
it('should parse a plain date string', () => {
const result = parseToPlainDateOrThrow('2024-03-15');
expect(result.year).toBe(2024);
expect(result.month).toBe(3);
expect(result.day).toBe(15);
});
it('should throw for an invalid date string', () => {
expect(() => parseToPlainDateOrThrow('not-a-date')).toThrow(
'Cannot parse date string as PlainDate',
);
});
});
describe('turnJSDateToPlainDate', () => {
it('should convert a JS Date to a PlainDate', () => {
const jsDate = new Date(2024, 2, 15); // March 15, 2024
const result = turnJSDateToPlainDate(jsDate);
expect(result.year).toBe(2024);
expect(result.month).toBe(3);
expect(result.day).toBe(15);
});
});
describe('turnPlainDateIntoUserTimeZoneInstantString', () => {
it('should convert a PlainDate to an instant string in the given timezone', () => {
const plainDate = Temporal.PlainDate.from('2024-03-15');
const result = turnPlainDateIntoUserTimeZoneInstantString(
plainDate,
'UTC',
);
expect(result).toBe('2024-03-15T00:00:00Z');
});
});
describe('turnPlainDateToShiftedDateInSystemTimeZone', () => {
it('should return a JS Date for the given PlainDate', () => {
const plainDate = Temporal.PlainDate.from('2024-03-15');
const result = turnPlainDateToShiftedDateInSystemTimeZone(plainDate);
expect(result).toBeInstanceOf(Date);
expect(result.getFullYear()).toBe(2024);
expect(result.getMonth()).toBe(2); // 0-indexed
expect(result.getDate()).toBe(15);
});
});
@@ -0,0 +1,132 @@
import { Temporal } from 'temporal-polyfill';
import { isPlainDateAfter } from '@/utils/date/isPlainDateAfter';
import { isPlainDateBefore } from '@/utils/date/isPlainDateBefore';
import { isPlainDateBeforeOrEqual } from '@/utils/date/isPlainDateBeforeOrEqual';
import { isPlainDateInSameMonth } from '@/utils/date/isPlainDateInSameMonth';
import { isPlainDateInWeekend } from '@/utils/date/isPlainDateInWeekend';
import { isSamePlainDate } from '@/utils/date/isSamePlainDate';
describe('isPlainDateAfter', () => {
it('should return true when first date is after second', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-10');
expect(isPlainDateAfter(a, b)).toBe(true);
});
it('should return false when first date is before second', () => {
const a = Temporal.PlainDate.from('2024-03-10');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateAfter(a, b)).toBe(false);
});
it('should return false when dates are equal', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateAfter(a, b)).toBe(false);
});
});
describe('isPlainDateBefore', () => {
it('should return true when first date is before second', () => {
const a = Temporal.PlainDate.from('2024-03-10');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateBefore(a, b)).toBe(true);
});
it('should return false when first date is after second', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-10');
expect(isPlainDateBefore(a, b)).toBe(false);
});
});
describe('isPlainDateBeforeOrEqual', () => {
it('should return true when first date is before second', () => {
const a = Temporal.PlainDate.from('2024-03-10');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateBeforeOrEqual(a, b)).toBe(true);
});
it('should return true when dates are equal', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateBeforeOrEqual(a, b)).toBe(true);
});
it('should return false when first date is after second', () => {
const a = Temporal.PlainDate.from('2024-03-20');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isPlainDateBeforeOrEqual(a, b)).toBe(false);
});
});
describe('isPlainDateInSameMonth', () => {
it('should return true for dates in same month and year', () => {
const a = Temporal.PlainDate.from('2024-03-01');
const b = Temporal.PlainDate.from('2024-03-31');
expect(isPlainDateInSameMonth(a, b)).toBe(true);
});
it('should return false for dates in different months', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-04-15');
expect(isPlainDateInSameMonth(a, b)).toBe(false);
});
it('should return false for same month but different year', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2025-03-15');
expect(isPlainDateInSameMonth(a, b)).toBe(false);
});
});
describe('isPlainDateInWeekend', () => {
it('should return true for Saturday', () => {
// 2024-03-16 is a Saturday
const saturday = Temporal.PlainDate.from('2024-03-16');
expect(isPlainDateInWeekend(saturday)).toBe(true);
});
it('should return true for Sunday', () => {
// 2024-03-17 is a Sunday
const sunday = Temporal.PlainDate.from('2024-03-17');
expect(isPlainDateInWeekend(sunday)).toBe(true);
});
it('should return false for a weekday', () => {
// 2024-03-18 is a Monday
const monday = Temporal.PlainDate.from('2024-03-18');
expect(isPlainDateInWeekend(monday)).toBe(false);
});
});
describe('isSamePlainDate', () => {
it('should return true for equal dates', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-15');
expect(isSamePlainDate(a, b)).toBe(true);
});
it('should return false for different dates', () => {
const a = Temporal.PlainDate.from('2024-03-15');
const b = Temporal.PlainDate.from('2024-03-16');
expect(isSamePlainDate(a, b)).toBe(false);
});
});
@@ -0,0 +1,18 @@
import { CustomError } from '@/utils/errors/CustomError';
describe('CustomError', () => {
it('should create an error with a message', () => {
const error = new CustomError('Something went wrong');
expect(error).toBeInstanceOf(Error);
expect(error.message).toBe('Something went wrong');
expect(error.code).toBeUndefined();
});
it('should create an error with a message and code', () => {
const error = new CustomError('Not found', 'NOT_FOUND');
expect(error.message).toBe('Not found');
expect(error.code).toBe('NOT_FOUND');
});
});
@@ -0,0 +1,36 @@
import { computeMorphRelationFieldName } from '@/utils/fieldMetadata/compute-morph-relation-field-name';
describe('computeMorphRelationFieldName', () => {
it('should return singular-based name for MANY_TO_ONE', () => {
const result = computeMorphRelationFieldName({
fieldName: 'assigned',
relationType: 'MANY_TO_ONE' as any,
targetObjectMetadataNameSingular: 'person',
targetObjectMetadataNamePlural: 'people',
});
expect(result).toBe('assignedPerson');
});
it('should return plural-based name for ONE_TO_MANY', () => {
const result = computeMorphRelationFieldName({
fieldName: 'assigned',
relationType: 'ONE_TO_MANY' as any,
targetObjectMetadataNameSingular: 'person',
targetObjectMetadataNamePlural: 'people',
});
expect(result).toBe('assignedPeople');
});
it('should throw for invalid relation type', () => {
expect(() =>
computeMorphRelationFieldName({
fieldName: 'assigned',
relationType: 'INVALID' as any,
targetObjectMetadataNameSingular: 'person',
targetObjectMetadataNamePlural: 'people',
}),
).toThrow();
});
});
@@ -0,0 +1,20 @@
import { FieldMetadataType } from '@/types';
import { isFieldMetadataDateKind } from '@/utils/fieldMetadata/isFieldMetadataDateKind';
describe('isFieldMetadataDateKind', () => {
it('should return true for DATE type', () => {
expect(isFieldMetadataDateKind(FieldMetadataType.DATE)).toBe(true);
});
it('should return true for DATE_TIME type', () => {
expect(isFieldMetadataDateKind(FieldMetadataType.DATE_TIME)).toBe(true);
});
it('should return false for TEXT type', () => {
expect(isFieldMetadataDateKind(FieldMetadataType.TEXT)).toBe(false);
});
it('should return false for undefined', () => {
expect(isFieldMetadataDateKind(undefined)).toBe(false);
});
});
@@ -0,0 +1,52 @@
import { FieldMetadataType, ViewFilterOperand } from '@/types';
import { computeEmptyGqlOperationFilterForEmails } from '@/utils/filter/computeEmptyGqlOperationFilterForEmails';
describe('computeEmptyGqlOperationFilterForEmails', () => {
const baseFilter = {
id: '1',
fieldMetadataId: 'f1',
value: '',
type: 'EMAILS' as const,
operand: ViewFilterOperand.IS_EMPTY,
};
const field = { name: 'email', type: FieldMetadataType.EMAILS };
it('should compute filter for primaryEmail subfield', () => {
const result = computeEmptyGqlOperationFilterForEmails({
recordFilter: { ...baseFilter, subFieldName: 'primaryEmail' },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('or');
expect((result as any).or).toHaveLength(2);
});
it('should compute filter for additionalEmails subfield', () => {
const result = computeEmptyGqlOperationFilterForEmails({
recordFilter: { ...baseFilter, subFieldName: 'additionalEmails' },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('or');
});
it('should throw for unknown subfield', () => {
expect(() =>
computeEmptyGqlOperationFilterForEmails({
recordFilter: { ...baseFilter, subFieldName: 'unknown' as any },
correspondingFieldMetadataItem: field,
}),
).toThrow('Unknown subfield name');
});
it('should compute combined filter when no subfield is specified', () => {
const result = computeEmptyGqlOperationFilterForEmails({
recordFilter: { ...baseFilter, subFieldName: undefined },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('and');
expect((result as any).and).toHaveLength(2);
});
});
@@ -0,0 +1,60 @@
import { ViewFilterOperand } from '@/types';
import { computeEmptyGqlOperationFilterForLinks } from '@/utils/filter/computeEmptyGqlOperationFilterForLinks';
describe('computeEmptyGqlOperationFilterForLinks', () => {
const baseFilter = {
id: '1',
fieldMetadataId: 'f1',
value: '',
type: 'LINKS' as const,
operand: ViewFilterOperand.IS_EMPTY,
};
const field = { name: 'links' };
it('should compute filter for primaryLinkLabel subfield', () => {
const result = computeEmptyGqlOperationFilterForLinks({
recordFilter: { ...baseFilter, subFieldName: 'primaryLinkLabel' },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('or');
});
it('should compute filter for primaryLinkUrl subfield', () => {
const result = computeEmptyGqlOperationFilterForLinks({
recordFilter: { ...baseFilter, subFieldName: 'primaryLinkUrl' },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('or');
});
it('should compute filter for secondaryLinks subfield', () => {
const result = computeEmptyGqlOperationFilterForLinks({
recordFilter: { ...baseFilter, subFieldName: 'secondaryLinks' },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('or');
});
it('should throw for unknown subfield', () => {
expect(() =>
computeEmptyGqlOperationFilterForLinks({
recordFilter: { ...baseFilter, subFieldName: 'unknown' as any },
correspondingFieldMetadataItem: field,
}),
).toThrow('Unknown subfield name');
});
it('should compute combined filter when no subfield is specified', () => {
const result = computeEmptyGqlOperationFilterForLinks({
recordFilter: { ...baseFilter, subFieldName: undefined },
correspondingFieldMetadataItem: field,
});
expect(result).toHaveProperty('and');
expect((result as any).and).toHaveLength(3);
});
});
@@ -0,0 +1,121 @@
import {
FieldMetadataType,
RecordFilterGroupLogicalOperator,
ViewFilterOperand,
} from '@/types';
import { turnRecordFilterGroupsIntoGqlOperationFilter } from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter';
describe('turnRecordFilterGroupsIntoGqlOperationFilter', () => {
const fields = [
{
id: 'f1',
name: 'name',
type: FieldMetadataType.TEXT,
label: 'Name',
},
{
id: 'f2',
name: 'age',
type: FieldMetadataType.NUMBER,
label: 'Age',
},
];
it('should return undefined when group is not found', () => {
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies: {},
filters: [],
fields,
recordFilterGroups: [],
currentRecordFilterGroupId: 'nonexistent',
});
expect(result).toBeUndefined();
});
it('should return AND filter for AND logical operator', () => {
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies: {},
filters: [
{
id: 'filter1',
fieldMetadataId: 'f1',
value: 'test',
type: 'TEXT',
operand: ViewFilterOperand.CONTAINS,
recordFilterGroupId: 'group1',
},
],
fields,
recordFilterGroups: [
{
id: 'group1',
logicalOperator: RecordFilterGroupLogicalOperator.AND,
},
],
currentRecordFilterGroupId: 'group1',
});
expect(result).toHaveProperty('and');
});
it('should return OR filter for OR logical operator', () => {
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies: {},
filters: [
{
id: 'filter1',
fieldMetadataId: 'f1',
value: 'test',
type: 'TEXT',
operand: ViewFilterOperand.CONTAINS,
recordFilterGroupId: 'group1',
},
],
fields,
recordFilterGroups: [
{
id: 'group1',
logicalOperator: RecordFilterGroupLogicalOperator.OR,
},
],
currentRecordFilterGroupId: 'group1',
});
expect(result).toHaveProperty('or');
});
it('should handle nested groups', () => {
const result = turnRecordFilterGroupsIntoGqlOperationFilter({
filterValueDependencies: {},
filters: [
{
id: 'filter1',
fieldMetadataId: 'f1',
value: 'test',
type: 'TEXT',
operand: ViewFilterOperand.CONTAINS,
recordFilterGroupId: 'subgroup1',
},
],
fields,
recordFilterGroups: [
{
id: 'group1',
logicalOperator: RecordFilterGroupLogicalOperator.AND,
},
{
id: 'subgroup1',
parentRecordFilterGroupId: 'group1',
logicalOperator: RecordFilterGroupLogicalOperator.OR,
},
],
currentRecordFilterGroupId: 'group1',
});
expect(result).toHaveProperty('and');
const andFilters = (result as any).and;
expect(andFilters.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,826 @@
import {
FieldMetadataType,
ViewFilterOperand as RecordFilterOperand,
} from '@/types';
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
import { type RecordFilter } from '@/utils';
const fields = [
{ id: 'f-text', name: 'name', type: FieldMetadataType.TEXT, label: 'Name' },
{
id: 'f-number',
name: 'amount',
type: FieldMetadataType.NUMBER,
label: 'Amount',
},
{
id: 'f-date',
name: 'createdAt',
type: FieldMetadataType.DATE,
label: 'Created At',
},
{
id: 'f-datetime',
name: 'updatedAt',
type: FieldMetadataType.DATE_TIME,
label: 'Updated At',
},
{
id: 'f-select',
name: 'status',
type: FieldMetadataType.SELECT,
label: 'Status',
},
{
id: 'f-multiselect',
name: 'tags',
type: FieldMetadataType.MULTI_SELECT,
label: 'Tags',
},
{
id: 'f-rating',
name: 'rating',
type: FieldMetadataType.RATING,
label: 'Rating',
},
{
id: 'f-relation',
name: 'company',
type: FieldMetadataType.RELATION,
label: 'Company',
},
{
id: 'f-bool',
name: 'isActive',
type: FieldMetadataType.BOOLEAN,
label: 'Is Active',
},
{
id: 'f-rawjson',
name: 'metadata',
type: FieldMetadataType.RAW_JSON,
label: 'Metadata',
},
{
id: 'f-files',
name: 'attachments',
type: FieldMetadataType.FILES,
label: 'Attachments',
},
{
id: 'f-tsvector',
name: 'search',
type: FieldMetadataType.TS_VECTOR,
label: 'Search',
},
{
id: 'f-currency',
name: 'revenue',
type: FieldMetadataType.CURRENCY,
label: 'Revenue',
},
{
id: 'f-fullname',
name: 'fullName',
type: FieldMetadataType.FULL_NAME,
label: 'Full Name',
},
{
id: 'f-address',
name: 'location',
type: FieldMetadataType.ADDRESS,
label: 'Location',
},
{
id: 'f-actor',
name: 'actor',
type: FieldMetadataType.ACTOR,
label: 'Actor',
},
{
id: 'f-phones',
name: 'phone',
type: FieldMetadataType.PHONES,
label: 'Phone',
},
{
id: 'f-emails',
name: 'email',
type: FieldMetadataType.EMAILS,
label: 'Email',
},
{
id: 'f-links',
name: 'website',
type: FieldMetadataType.LINKS,
label: 'Website',
},
{
id: 'f-array',
name: 'items',
type: FieldMetadataType.ARRAY,
label: 'Items',
},
{
id: 'f-uuid',
name: 'recordId',
type: FieldMetadataType.UUID,
label: 'Record ID',
},
];
const filterValueDependencies = { timeZone: 'UTC' };
const makeFilter = (
fieldMetadataId: string,
operand: RecordFilterOperand,
value: string,
type: string = 'TEXT',
subFieldName?: string,
) =>
({
id: 'test-filter',
fieldMetadataId,
value,
type: type as any,
operand,
subFieldName,
}) as RecordFilter;
describe('turnRecordFilterIntoRecordGqlOperationFilter', () => {
it('should return undefined when field metadata is not found', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'nonexistent',
RecordFilterOperand.CONTAINS,
'x',
),
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
});
it('should return undefined when value is empty for non-emptiness operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-text', RecordFilterOperand.CONTAINS, ''),
fieldMetadataItems: fields,
});
expect(result).toBeUndefined();
});
describe('TEXT filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-text',
RecordFilterOperand.CONTAINS,
'test',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ name: { ilike: '%test%' } });
});
it('should handle DOES_NOT_CONTAIN operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-text',
RecordFilterOperand.DOES_NOT_CONTAIN,
'test',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { name: { ilike: '%test%' } } });
});
});
describe('NUMBER filter', () => {
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-number', RecordFilterOperand.IS, '42'),
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { eq: 42 } });
});
it('should handle IS_NOT operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-number', RecordFilterOperand.IS_NOT, '42'),
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { amount: { eq: 42 } } });
});
it('should handle GREATER_THAN_OR_EQUAL operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-number',
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
'10',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { gte: 10 } });
});
it('should handle LESS_THAN_OR_EQUAL operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-number',
RecordFilterOperand.LESS_THAN_OR_EQUAL,
'100',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ amount: { lte: 100 } });
});
});
describe('DATE filter', () => {
it('should handle IS_AFTER operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-date',
RecordFilterOperand.IS_AFTER,
'2024-03-15',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { gte: '2024-03-15' } });
});
it('should handle IS_BEFORE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-date',
RecordFilterOperand.IS_BEFORE,
'2024-03-15',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { lt: '2024-03-15' } });
});
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-date',
RecordFilterOperand.IS,
'2024-03-15',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ createdAt: { eq: '2024-03-15' } });
});
it('should handle IS_IN_PAST operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_IN_PAST, ''),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.lt');
});
it('should handle IS_IN_FUTURE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-date',
RecordFilterOperand.IS_IN_FUTURE,
'',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.gte');
});
it('should handle IS_TODAY operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-date', RecordFilterOperand.IS_TODAY, ''),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('createdAt.eq');
});
it('should handle IS_RELATIVE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-date',
RecordFilterOperand.IS_RELATIVE,
'PAST_7_DAY',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
});
});
describe('DATE_TIME filter', () => {
it('should handle IS_AFTER operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_AFTER,
'2024-03-15T10:00:00Z',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.gte');
});
it('should handle IS_BEFORE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_BEFORE,
'2024-03-15T10:00:00Z',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.lt');
});
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS,
'2024-03-15T10:00:00Z',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
});
it('should handle IS_IN_PAST operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_IN_PAST,
'',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.lt');
});
it('should handle IS_IN_FUTURE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_IN_FUTURE,
'',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('updatedAt.gt');
});
it('should handle IS_TODAY operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_TODAY,
'',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
});
it('should handle IS_RELATIVE operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-datetime',
RecordFilterOperand.IS_RELATIVE,
`PAST_7_DAY;;UTC;;`,
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('and');
});
});
describe('RATING filter', () => {
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-rating', RecordFilterOperand.IS, '3'),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.eq');
});
it('should handle GREATER_THAN_OR_EQUAL operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-rating',
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
'3',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.in');
});
it('should handle LESS_THAN_OR_EQUAL operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-rating',
RecordFilterOperand.LESS_THAN_OR_EQUAL,
'3',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('rating.in');
});
});
describe('BOOLEAN filter', () => {
it('should handle IS operand with true', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'true'),
fieldMetadataItems: fields,
});
expect(result).toEqual({ isActive: { eq: true } });
});
it('should handle IS operand with false', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter('f-bool', RecordFilterOperand.IS, 'false'),
fieldMetadataItems: fields,
});
expect(result).toEqual({ isActive: { eq: false } });
});
});
describe('SELECT filter', () => {
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-select',
RecordFilterOperand.IS,
'["ACTIVE","PENDING"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('status.in');
});
it('should handle IS_NOT operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-select',
RecordFilterOperand.IS_NOT,
'["ACTIVE"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('not');
});
});
describe('MULTI_SELECT filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-multiselect',
RecordFilterOperand.CONTAINS,
'["TAG1","TAG2"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('tags.containsAny');
});
it('should handle DOES_NOT_CONTAIN operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-multiselect',
RecordFilterOperand.DOES_NOT_CONTAIN,
'["TAG1"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
});
});
describe('RELATION filter', () => {
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-relation',
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('companyId.in');
});
it('should handle IS_NOT operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-relation',
RecordFilterOperand.IS_NOT,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
});
});
describe('RAW_JSON filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-rawjson',
RecordFilterOperand.CONTAINS,
'test',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ metadata: { like: '%test%' } });
});
it('should handle DOES_NOT_CONTAIN operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-rawjson',
RecordFilterOperand.DOES_NOT_CONTAIN,
'test',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ not: { metadata: { like: '%test%' } } });
});
});
describe('FILES filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-files',
RecordFilterOperand.CONTAINS,
'doc',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ attachments: { like: '%doc%' } });
});
});
describe('TS_VECTOR filter', () => {
it('should handle VECTOR_SEARCH operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-tsvector',
RecordFilterOperand.VECTOR_SEARCH,
'hello world',
),
fieldMetadataItems: fields,
});
expect(result).toEqual({ search: { search: 'hello world' } });
});
});
describe('CURRENCY filter', () => {
it('should handle GREATER_THAN_OR_EQUAL on amountMicros', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-currency',
RecordFilterOperand.GREATER_THAN_OR_EQUAL,
'1000',
'CURRENCY',
'amountMicros',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('revenue');
});
});
describe('FULL_NAME filter', () => {
it('should handle CONTAINS operand without subfield', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-fullname',
RecordFilterOperand.CONTAINS,
'John',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
});
});
describe('ADDRESS filter', () => {
it('should handle CONTAINS operand without subfield', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-address',
RecordFilterOperand.CONTAINS,
'Paris',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('or');
});
});
describe('ACTOR filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-actor',
RecordFilterOperand.CONTAINS,
'Admin',
),
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
});
});
describe('PHONES filter', () => {
it('should handle CONTAINS on primaryPhoneNumber', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-phones',
RecordFilterOperand.CONTAINS,
'555',
'PHONES',
'primaryPhoneNumber',
),
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
});
});
describe('EMAILS filter', () => {
it('should handle CONTAINS on primaryEmail', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-emails',
RecordFilterOperand.CONTAINS,
'test@example.com',
'EMAILS',
'primaryEmail',
),
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
});
});
describe('LINKS filter', () => {
it('should handle CONTAINS on primaryLinkUrl', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-links',
RecordFilterOperand.CONTAINS,
'example.com',
'LINKS',
'primaryLinkUrl',
),
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
});
});
describe('ARRAY filter', () => {
it('should handle CONTAINS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-array',
RecordFilterOperand.CONTAINS,
'["item1"]',
),
fieldMetadataItems: fields,
});
expect(result).toBeDefined();
});
it('should handle DOES_NOT_CONTAIN operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-array',
RecordFilterOperand.DOES_NOT_CONTAIN,
'["item1"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('not');
});
});
describe('UUID filter', () => {
it('should handle IS operand', () => {
const result = turnRecordFilterIntoRecordGqlOperationFilter({
filterValueDependencies,
recordFilter: makeFilter(
'f-uuid',
RecordFilterOperand.IS,
'["550e8400-e29b-41d4-a716-446655440000"]',
),
fieldMetadataItems: fields,
});
expect(result).toHaveProperty('recordId.in');
});
});
});
@@ -0,0 +1,47 @@
import { addUnitToDateTime } from '@/utils/filter/dates/utils/addUnitToDateTime';
describe('addUnitToDateTime', () => {
const baseDate = new Date('2024-03-15T12:00:00Z');
it('should add seconds', () => {
const result = addUnitToDateTime(baseDate, 30, 'SECOND');
expect(result.getTime() - baseDate.getTime()).toBe(30_000);
});
it('should add minutes', () => {
const result = addUnitToDateTime(baseDate, 5, 'MINUTE');
expect(result.getTime() - baseDate.getTime()).toBe(5 * 60_000);
});
it('should add hours', () => {
const result = addUnitToDateTime(baseDate, 2, 'HOUR');
expect(result.getTime() - baseDate.getTime()).toBe(2 * 3_600_000);
});
it('should add days', () => {
const result = addUnitToDateTime(baseDate, 3, 'DAY');
expect(result.getDate()).toBe(18);
});
it('should add weeks', () => {
const result = addUnitToDateTime(baseDate, 1, 'WEEK');
expect(result.getDate()).toBe(22);
});
it('should add months', () => {
const result = addUnitToDateTime(baseDate, 2, 'MONTH');
expect(result.getMonth()).toBe(4); // May (0-indexed)
});
it('should add years', () => {
const result = addUnitToDateTime(baseDate, 1, 'YEAR');
expect(result.getFullYear()).toBe(2025);
});
});
@@ -0,0 +1,57 @@
import { Temporal } from 'temporal-polyfill';
import { addUnitToZonedDateTime } from '@/utils/filter/dates/utils/addUnitToZonedDateTime';
describe('addUnitToZonedDateTime', () => {
const baseZdt = Temporal.ZonedDateTime.from(
'2024-03-15T12:00:00[UTC]',
);
it('should add days', () => {
const result = addUnitToZonedDateTime(baseZdt, 'DAY', 3);
expect(result.day).toBe(18);
});
it('should add weeks', () => {
const result = addUnitToZonedDateTime(baseZdt, 'WEEK', 1);
expect(result.day).toBe(22);
});
it('should add months', () => {
const result = addUnitToZonedDateTime(baseZdt, 'MONTH', 2);
expect(result.month).toBe(5);
});
it('should add quarters', () => {
const result = addUnitToZonedDateTime(baseZdt, 'QUARTER', 1);
expect(result.month).toBe(6);
});
it('should add years', () => {
const result = addUnitToZonedDateTime(baseZdt, 'YEAR', 1);
expect(result.year).toBe(2025);
});
it('should add seconds', () => {
const result = addUnitToZonedDateTime(baseZdt, 'SECOND', 30);
expect(result.second).toBe(30);
});
it('should add minutes', () => {
const result = addUnitToZonedDateTime(baseZdt, 'MINUTE', 15);
expect(result.minute).toBe(15);
});
it('should add hours', () => {
const result = addUnitToZonedDateTime(baseZdt, 'HOUR', 3);
expect(result.hour).toBe(15);
});
});
@@ -0,0 +1,89 @@
import { CalendarStartDay } from '@/constants';
import { FirstDayOfTheWeek } from '@/types';
import { convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek } from '@/utils/filter/dates/utils/convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek';
import { convertFirstDayOfTheWeekToCalendarStartDayNumber } from '@/utils/filter/dates/utils/convertFirstDayOfTheWeekToCalendarStartDayNumber';
import { getFirstDayOfTheWeekAsANumberForDateFNS } from '@/utils/filter/dates/utils/getFirstDayOfTheWeekAsANumberForDateFNS';
describe('convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek', () => {
it('should convert MONDAY', () => {
expect(
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
CalendarStartDay.MONDAY,
FirstDayOfTheWeek.SUNDAY,
),
).toBe(FirstDayOfTheWeek.MONDAY);
});
it('should convert SATURDAY', () => {
expect(
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
CalendarStartDay.SATURDAY,
FirstDayOfTheWeek.SUNDAY,
),
).toBe(FirstDayOfTheWeek.SATURDAY);
});
it('should convert SUNDAY', () => {
expect(
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
CalendarStartDay.SUNDAY,
FirstDayOfTheWeek.MONDAY,
),
).toBe(FirstDayOfTheWeek.SUNDAY);
});
it('should use system default for SYSTEM', () => {
expect(
convertCalendarStartDayNonIsoNumberToFirstDayOfTheWeek(
CalendarStartDay.SYSTEM,
FirstDayOfTheWeek.SATURDAY,
),
).toBe(FirstDayOfTheWeek.SATURDAY);
});
});
describe('convertFirstDayOfTheWeekToCalendarStartDayNumber', () => {
it('should convert MONDAY', () => {
expect(
convertFirstDayOfTheWeekToCalendarStartDayNumber(
FirstDayOfTheWeek.MONDAY,
),
).toBe(CalendarStartDay.MONDAY);
});
it('should convert SATURDAY', () => {
expect(
convertFirstDayOfTheWeekToCalendarStartDayNumber(
FirstDayOfTheWeek.SATURDAY,
),
).toBe(CalendarStartDay.SATURDAY);
});
it('should convert SUNDAY', () => {
expect(
convertFirstDayOfTheWeekToCalendarStartDayNumber(
FirstDayOfTheWeek.SUNDAY,
),
).toBe(CalendarStartDay.SUNDAY);
});
});
describe('getFirstDayOfTheWeekAsANumberForDateFNS', () => {
it('should return 1 for MONDAY', () => {
expect(
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.MONDAY),
).toBe(1);
});
it('should return 6 for SATURDAY', () => {
expect(
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.SATURDAY),
).toBe(6);
});
it('should return 0 for SUNDAY', () => {
expect(
getFirstDayOfTheWeekAsANumberForDateFNS(FirstDayOfTheWeek.SUNDAY),
).toBe(0);
});
});
@@ -0,0 +1,69 @@
import { relativeDateFilterStringifiedSchema } from '@/utils/filter/dates/utils/relativeDateFilterStringifiedSchema';
describe('relativeDateFilterStringifiedSchema', () => {
it('should parse a valid PAST filter string', () => {
const result = relativeDateFilterStringifiedSchema.safeParse('PAST_7_DAY');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('PAST');
expect(result.data.amount).toBe(7);
expect(result.data.unit).toBe('DAY');
}
});
it('should parse a valid NEXT filter string', () => {
const result = relativeDateFilterStringifiedSchema.safeParse('NEXT_30_MINUTE');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('NEXT');
expect(result.data.amount).toBe(30);
expect(result.data.unit).toBe('MINUTE');
}
});
it('should parse a THIS filter string', () => {
const result = relativeDateFilterStringifiedSchema.safeParse('THIS_1_MONTH');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.direction).toBe('THIS');
expect(result.data.unit).toBe('MONTH');
}
});
it('should parse a filter string with timezone', () => {
const result = relativeDateFilterStringifiedSchema.safeParse(
'PAST_7_DAY;;America/New_York;;',
);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.timezone).toBe('America/New_York');
}
});
it('should parse a filter string with firstDayOfTheWeek', () => {
const result = relativeDateFilterStringifiedSchema.safeParse(
'THIS_1_WEEK;;UTC;;MONDAY;;',
);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.firstDayOfTheWeek).toBe('MONDAY');
}
});
it('should fail for invalid strings', () => {
const result =
relativeDateFilterStringifiedSchema.safeParse('INVALID_STRING');
expect(result.success).toBe(false);
});
});
@@ -0,0 +1,62 @@
import { ViewFilterOperand } from '@/types';
import { resolveDateFilter } from '@/utils/filter/dates/utils/resolveDateFilter';
import { resolveDateTimeFilter } from '@/utils/filter/dates/utils/resolveDateTimeFilter';
describe('resolveDateFilter', () => {
it('should return null for empty value', () => {
expect(
resolveDateFilter({ value: '', operand: ViewFilterOperand.IS_AFTER }),
).toBeNull();
});
it('should return the value directly for non-relative operands', () => {
expect(
resolveDateFilter({
value: '2024-03-15',
operand: ViewFilterOperand.IS_AFTER,
}),
).toBe('2024-03-15');
});
it('should resolve relative date filter for IS_RELATIVE operand', () => {
const result = resolveDateFilter({
value: 'PAST_7_DAY',
operand: ViewFilterOperand.IS_RELATIVE,
});
expect(result).not.toBeNull();
expect(result).toHaveProperty('start');
expect(result).toHaveProperty('end');
});
});
describe('resolveDateTimeFilter', () => {
it('should return null for empty value', () => {
expect(
resolveDateTimeFilter({
value: '',
operand: ViewFilterOperand.IS_AFTER,
}),
).toBeNull();
});
it('should return the value directly for non-relative operands', () => {
expect(
resolveDateTimeFilter({
value: '2024-03-15T10:00:00Z',
operand: ViewFilterOperand.IS_AFTER,
}),
).toBe('2024-03-15T10:00:00Z');
});
it('should resolve relative date-time filter for IS_RELATIVE operand', () => {
const result = resolveDateTimeFilter({
value: 'PAST_7_DAY',
operand: ViewFilterOperand.IS_RELATIVE,
});
expect(result).not.toBeNull();
expect(result).toHaveProperty('start');
expect(result).toHaveProperty('end');
});
});
@@ -0,0 +1,73 @@
import { Temporal } from 'temporal-polyfill';
import { resolveRelativeDateFilter } from '@/utils/filter/dates/utils/resolveRelativeDateFilter';
describe('resolveRelativeDateFilter', () => {
const referenceZdt = Temporal.ZonedDateTime.from(
'2024-03-15T12:00:00[UTC]',
);
describe('NEXT direction', () => {
it('should compute start and end for NEXT 7 DAY', () => {
const result = resolveRelativeDateFilter(
{ direction: 'NEXT', amount: 7, unit: 'DAY' },
referenceZdt,
);
expect(result.start).toBe('2024-03-16');
expect(result.end).toBe('2024-03-23');
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateFilter(
{ direction: 'NEXT', amount: undefined as any, unit: 'DAY' },
referenceZdt,
),
).toThrow('Amount is required');
});
});
describe('PAST direction', () => {
it('should compute start and end for PAST 7 DAY', () => {
const result = resolveRelativeDateFilter(
{ direction: 'PAST', amount: 7, unit: 'DAY' },
referenceZdt,
);
expect(result.start).toBe('2024-03-08');
expect(result.end).toBe('2024-03-15');
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateFilter(
{ direction: 'PAST', amount: undefined as any, unit: 'DAY' },
referenceZdt,
),
).toThrow('Amount is required');
});
});
describe('THIS direction', () => {
it('should compute start and end for THIS MONTH', () => {
const result = resolveRelativeDateFilter(
{ direction: 'THIS', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start).toBe('2024-03-01');
expect(result.end).toBe('2024-04-01');
});
it('should compute start and end for THIS YEAR', () => {
const result = resolveRelativeDateFilter(
{ direction: 'THIS', amount: 1, unit: 'YEAR' },
referenceZdt,
);
expect(result.start).toBe('2024-01-01');
expect(result.end).toBe('2025-01-01');
});
});
});
@@ -0,0 +1,59 @@
import { resolveRelativeDateFilterStringified } from '@/utils/filter/dates/utils/resolveRelativeDateFilterStringified';
import { resolveRelativeDateTimeFilterStringified } from '@/utils/filter/dates/utils/resolveRelativeDateTimeFilterStringified';
describe('resolveRelativeDateFilterStringified', () => {
it('should return null for empty string', () => {
expect(resolveRelativeDateFilterStringified('')).toBeNull();
});
it('should return null for null', () => {
expect(resolveRelativeDateFilterStringified(null)).toBeNull();
});
it('should return null for undefined', () => {
expect(resolveRelativeDateFilterStringified(undefined)).toBeNull();
});
it('should return null for invalid filter string', () => {
expect(resolveRelativeDateFilterStringified('INVALID')).toBeNull();
});
it('should resolve a valid PAST filter', () => {
const result = resolveRelativeDateFilterStringified('PAST_7_DAY');
expect(result).not.toBeNull();
expect(result?.direction).toBe('PAST');
expect(result?.start).toBeDefined();
expect(result?.end).toBeDefined();
});
it('should resolve a valid NEXT filter', () => {
const result = resolveRelativeDateFilterStringified('NEXT_3_MONTH');
expect(result).not.toBeNull();
expect(result?.direction).toBe('NEXT');
});
});
describe('resolveRelativeDateTimeFilterStringified', () => {
it('should return null for empty string', () => {
expect(resolveRelativeDateTimeFilterStringified('')).toBeNull();
});
it('should return null for null', () => {
expect(resolveRelativeDateTimeFilterStringified(null)).toBeNull();
});
it('should return null for invalid filter string', () => {
expect(resolveRelativeDateTimeFilterStringified('INVALID')).toBeNull();
});
it('should resolve a valid PAST filter', () => {
const result = resolveRelativeDateTimeFilterStringified('PAST_24_HOUR');
expect(result).not.toBeNull();
expect(result?.direction).toBe('PAST');
expect(result?.start).toBeDefined();
expect(result?.end).toBeDefined();
});
});
@@ -0,0 +1,84 @@
import { Temporal } from 'temporal-polyfill';
import { resolveRelativeDateTimeFilter } from '@/utils/filter/dates/utils/resolveRelativeDateTimeFilter';
describe('resolveRelativeDateTimeFilter', () => {
const referenceZdt = Temporal.ZonedDateTime.from(
'2024-03-15T12:00:00[UTC]',
);
describe('NEXT direction', () => {
it('should compute for sub-day unit (HOUR)', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: 3, unit: 'HOUR' },
referenceZdt,
);
expect(result.start).toEqual(referenceZdt);
expect(result.end?.hour).toBe(15);
});
it('should compute for DAY unit', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: 7, unit: 'DAY' },
referenceZdt,
);
expect(result.start?.day).toBe(16);
expect(result.end?.day).toBe(23);
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateTimeFilter(
{ direction: 'NEXT', amount: undefined as any, unit: 'DAY' },
referenceZdt,
),
).toThrow('Amount is required');
});
});
describe('PAST direction', () => {
it('should compute for sub-day unit (MINUTE)', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 30, unit: 'MINUTE' },
referenceZdt,
);
expect(result.end).toEqual(referenceZdt);
expect(result.start?.minute).toBe(30);
expect(result.start?.hour).toBe(11);
});
it('should compute for DAY unit', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: 3, unit: 'DAY' },
referenceZdt,
);
expect(result.end?.hour).toBe(0);
expect(result.start?.day).toBe(12);
});
it('should throw if amount is undefined', () => {
expect(() =>
resolveRelativeDateTimeFilter(
{ direction: 'PAST', amount: undefined as any, unit: 'MONTH' },
referenceZdt,
),
).toThrow('Amount is required');
});
});
describe('THIS direction', () => {
it('should compute for THIS MONTH', () => {
const result = resolveRelativeDateTimeFilter(
{ direction: 'THIS', amount: 1, unit: 'MONTH' },
referenceZdt,
);
expect(result.start?.day).toBe(1);
expect(result.end?.month).toBe(4);
});
});
});
@@ -0,0 +1,47 @@
import { subUnitFromDateTime } from '@/utils/filter/dates/utils/subUnitFromDateTime';
describe('subUnitFromDateTime', () => {
const baseDate = new Date('2024-03-15T12:00:00Z');
it('should subtract seconds', () => {
const result = subUnitFromDateTime(baseDate, 30, 'SECOND');
expect(baseDate.getTime() - result.getTime()).toBe(30_000);
});
it('should subtract minutes', () => {
const result = subUnitFromDateTime(baseDate, 5, 'MINUTE');
expect(baseDate.getTime() - result.getTime()).toBe(5 * 60_000);
});
it('should subtract hours', () => {
const result = subUnitFromDateTime(baseDate, 2, 'HOUR');
expect(baseDate.getTime() - result.getTime()).toBe(2 * 3_600_000);
});
it('should subtract days', () => {
const result = subUnitFromDateTime(baseDate, 3, 'DAY');
expect(result.getDate()).toBe(12);
});
it('should subtract weeks', () => {
const result = subUnitFromDateTime(baseDate, 1, 'WEEK');
expect(result.getDate()).toBe(8);
});
it('should subtract months', () => {
const result = subUnitFromDateTime(baseDate, 2, 'MONTH');
expect(result.getMonth()).toBe(0); // January
});
it('should subtract years', () => {
const result = subUnitFromDateTime(baseDate, 1, 'YEAR');
expect(result.getFullYear()).toBe(2023);
});
});
@@ -0,0 +1,60 @@
import { Temporal } from 'temporal-polyfill';
import { subUnitFromZonedDateTime } from '@/utils/filter/dates/utils/subUnitFromZonedDateTime';
describe('subUnitFromZonedDateTime', () => {
const baseZdt = Temporal.ZonedDateTime.from(
'2024-03-15T12:00:00[UTC]',
);
it('should subtract days', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'DAY', 3);
expect(result.day).toBe(12);
});
it('should subtract weeks', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'WEEK', 1);
expect(result.day).toBe(8);
});
it('should subtract months', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'MONTH', 2);
expect(result.month).toBe(1);
});
it('should subtract quarters', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'QUARTER', 1);
expect(result.month).toBe(12);
expect(result.year).toBe(2023);
});
it('should subtract years', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'YEAR', 1);
expect(result.year).toBe(2023);
});
it('should subtract seconds', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'SECOND', 30);
expect(result.second).toBe(30);
expect(result.minute).toBe(59);
expect(result.hour).toBe(11);
});
it('should subtract minutes', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'MINUTE', 15);
expect(result.minute).toBe(45);
});
it('should subtract hours', () => {
const result = subUnitFromZonedDateTime(baseZdt, 'HOUR', 3);
expect(result.hour).toBe(9);
});
});
@@ -0,0 +1,26 @@
import { combineFilters } from '@/utils/filter/utils/combineFilters';
describe('combineFilters', () => {
it('should return empty object when all filters are empty', () => {
expect(combineFilters([{}, {}, {}])).toEqual({});
});
it('should return single filter when only one non-empty filter exists', () => {
const filter = { name: { eq: 'test' } };
expect(combineFilters([{}, filter, {}])).toEqual(filter);
});
it('should combine multiple non-empty filters with and', () => {
const filter1 = { name: { eq: 'test' } };
const filter2 = { age: { gt: 18 } };
expect(combineFilters([filter1, filter2])).toEqual({
and: [filter1, filter2],
});
});
it('should return empty object for empty array', () => {
expect(combineFilters([])).toEqual({});
});
});
@@ -0,0 +1,25 @@
import { convertViewFilterValueToString } from '@/utils/filter/utils/convertViewFilterValueToString';
describe('convertViewFilterValueToString', () => {
it('should return string values as-is', () => {
expect(convertViewFilterValueToString('hello')).toBe('hello');
});
it('should stringify non-string values', () => {
expect(convertViewFilterValueToString(42)).toBe('42');
});
it('should stringify objects', () => {
expect(convertViewFilterValueToString({ key: 'value' })).toBe(
'{"key":"value"}',
);
});
it('should stringify null as empty string', () => {
expect(convertViewFilterValueToString(null)).toBe('""');
});
it('should stringify undefined as empty string', () => {
expect(convertViewFilterValueToString(undefined)).toBe('""');
});
});
@@ -0,0 +1,239 @@
import { FieldMetadataType, ViewFilterOperand } from '@/types';
import { getEmptyRecordGqlOperationFilter } from '@/utils/filter/utils/getEmptyRecordGqlOperationFilter';
import { type RecordFilter } from '@/utils';
const makeParams = (
fieldType: FieldMetadataType,
operand: ViewFilterOperand = ViewFilterOperand.IS_EMPTY,
subFieldName?: string,
) => ({
operand,
correspondingField: { id: 'f1', name: 'testField', type: fieldType },
recordFilter: {
id: '1',
fieldMetadataId: 'f1',
value: '',
type: 'TEXT' as any,
operand,
subFieldName,
} as RecordFilter,
});
describe('getEmptyRecordGqlOperationFilter', () => {
describe('IS_EMPTY operand', () => {
it('should handle TEXT type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.TEXT),
);
expect(result).toHaveProperty('or');
});
it('should handle NUMBER type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.NUMBER),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle DATE type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.DATE),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle DATE_TIME type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.DATE_TIME),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle SELECT type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.SELECT),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle MULTI_SELECT type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.MULTI_SELECT),
);
expect(result).toHaveProperty('or');
});
it('should handle RATING type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.RATING),
);
expect(result).toEqual({
testField: { is: 'NULL' },
});
});
it('should handle RELATION type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.RELATION),
);
expect(result).toEqual({
testFieldId: { is: 'NULL' },
});
});
it('should handle CURRENCY type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.CURRENCY),
);
expect(result).toHaveProperty('or');
});
it('should handle ACTOR type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.ACTOR),
);
expect(result).toHaveProperty('or');
});
it('should handle ARRAY type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.ARRAY),
);
expect(result).toHaveProperty('or');
});
it('should handle RAW_JSON type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.RAW_JSON),
);
expect(result).toHaveProperty('or');
});
it('should handle FILES type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.FILES),
);
expect(result).toHaveProperty('or');
});
it('should handle FULL_NAME type without subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.FULL_NAME),
);
expect(result).toHaveProperty('and');
});
it('should handle FULL_NAME type with subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(
FieldMetadataType.FULL_NAME,
ViewFilterOperand.IS_EMPTY,
'firstName',
),
);
expect(result).toHaveProperty('or');
});
it('should handle PHONES type without subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.PHONES),
);
expect(result).toHaveProperty('and');
});
it('should handle PHONES type with primaryPhoneNumber subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(
FieldMetadataType.PHONES,
ViewFilterOperand.IS_EMPTY,
'primaryPhoneNumber',
),
);
expect(result).toHaveProperty('or');
});
it('should handle PHONES type with additionalPhones subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(
FieldMetadataType.PHONES,
ViewFilterOperand.IS_EMPTY,
'additionalPhones',
),
);
expect(result).toHaveProperty('or');
});
it('should handle ADDRESS type without subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.ADDRESS),
);
expect(result).toHaveProperty('and');
expect((result as any).and).toHaveLength(6);
});
it('should handle ADDRESS type with subfield', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(
FieldMetadataType.ADDRESS,
ViewFilterOperand.IS_EMPTY,
'addressCity',
),
);
expect(result).toHaveProperty('or');
});
it('should handle LINKS type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.LINKS),
);
expect(result).toHaveProperty('and');
});
it('should handle EMAILS type', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.EMAILS),
);
expect(result).toHaveProperty('and');
});
});
describe('IS_NOT_EMPTY operand', () => {
it('should wrap filter in not for IS_NOT_EMPTY', () => {
const result = getEmptyRecordGqlOperationFilter(
makeParams(FieldMetadataType.TEXT, ViewFilterOperand.IS_NOT_EMPTY),
);
expect(result).toHaveProperty('not');
});
});
});
@@ -0,0 +1,83 @@
import { isMatchingMultiSelectFilter } from '@/utils/filter/utils/isMatchingMultiSelectFilter';
describe('isMatchingMultiSelectFilter', () => {
describe('containsAny', () => {
it('should return true when value contains all filter items', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { containsAny: ['A', 'B'] },
value: ['A', 'B', 'C'],
}),
).toBe(true);
});
it('should return false when value does not contain all filter items', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { containsAny: ['A', 'D'] },
value: ['A', 'B', 'C'],
}),
).toBe(false);
});
it('should return false for null value', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { containsAny: ['A'] },
value: null,
}),
).toBe(false);
});
});
describe('isEmptyArray', () => {
it('should return true for empty array', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { isEmptyArray: true },
value: [],
}),
).toBe(true);
});
it('should return false for non-empty array', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { isEmptyArray: true },
value: ['A'],
}),
).toBe(false);
});
});
describe('is', () => {
it('should match NULL check', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { is: 'NULL' },
value: null,
}),
).toBe(true);
});
it('should match NOT_NULL check', () => {
expect(
isMatchingMultiSelectFilter({
multiSelectFilter: { is: 'NOT_NULL' },
value: ['A'],
}),
).toBe(true);
});
});
describe('default', () => {
it('should throw for unexpected filter', () => {
expect(() =>
isMatchingMultiSelectFilter({
multiSelectFilter: {} as any,
value: ['A'],
}),
).toThrow('Unexpected value for multi-select filter');
});
});
});
@@ -0,0 +1,74 @@
import { isMatchingRatingFilter } from '@/utils/filter/utils/isMatchingRatingFilter';
describe('isMatchingRatingFilter', () => {
describe('eq', () => {
it('should return true when value equals', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { eq: 'RATING_3' },
value: 'RATING_3',
}),
).toBe(true);
});
it('should return false when value does not equal', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { eq: 'RATING_3' },
value: 'RATING_1',
}),
).toBe(false);
});
});
describe('in', () => {
it('should return true when value is in list', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { in: ['RATING_3', 'RATING_5'] },
value: 'RATING_3',
}),
).toBe(true);
});
it('should return false for null value', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { in: ['RATING_3'] },
value: null,
}),
).toBe(false);
});
});
describe('is', () => {
it('should match NULL check', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { is: 'NULL' },
value: null,
}),
).toBe(true);
});
it('should match NOT_NULL check', () => {
expect(
isMatchingRatingFilter({
ratingFilter: { is: 'NOT_NULL' },
value: 'RATING_3',
}),
).toBe(true);
});
});
describe('default', () => {
it('should throw for unexpected filter', () => {
expect(() =>
isMatchingRatingFilter({
ratingFilter: {} as any,
value: 'RATING_3',
}),
).toThrow('Unexpected value for rating filter');
});
});
});
@@ -0,0 +1,54 @@
import { isMatchingRawJsonFilter } from '@/utils/filter/utils/isMatchingRawJsonFilter';
describe('isMatchingRawJsonFilter', () => {
describe('like', () => {
it('should match using wildcard pattern', () => {
expect(
isMatchingRawJsonFilter({
rawJsonFilter: { like: '%test%' },
value: 'some test value',
}),
).toBe(true);
});
it('should not match when pattern does not match', () => {
expect(
isMatchingRawJsonFilter({
rawJsonFilter: { like: '%xyz%' },
value: 'some test value',
}),
).toBe(false);
});
});
describe('is', () => {
it('should match NULL check', () => {
expect(
isMatchingRawJsonFilter({
rawJsonFilter: { is: 'NULL' },
value: null as any,
}),
).toBe(true);
});
it('should match NOT_NULL check', () => {
expect(
isMatchingRawJsonFilter({
rawJsonFilter: { is: 'NOT_NULL' },
value: '{"key": "val"}',
}),
).toBe(true);
});
});
describe('default', () => {
it('should throw for unexpected filter', () => {
expect(() =>
isMatchingRawJsonFilter({
rawJsonFilter: {} as any,
value: 'test',
}),
).toThrow('Unexpected value for string filter');
});
});
});
@@ -0,0 +1,43 @@
import { isMatchingRichTextV2Filter } from '@/utils/filter/utils/isMatchingRichTextV2Filter';
describe('isMatchingRichTextV2Filter', () => {
describe('markdown ilike', () => {
it('should match with wildcard pattern', () => {
expect(
isMatchingRichTextV2Filter({
richTextV2Filter: { markdown: { ilike: '%hello%' } },
value: 'say hello world',
}),
).toBe(true);
});
it('should not match when pattern does not match', () => {
expect(
isMatchingRichTextV2Filter({
richTextV2Filter: { markdown: { ilike: '%goodbye%' } },
value: 'say hello world',
}),
).toBe(false);
});
it('should be case insensitive', () => {
expect(
isMatchingRichTextV2Filter({
richTextV2Filter: { markdown: { ilike: '%HELLO%' } },
value: 'say hello world',
}),
).toBe(true);
});
});
describe('default', () => {
it('should throw for unexpected filter', () => {
expect(() =>
isMatchingRichTextV2Filter({
richTextV2Filter: {} as any,
value: 'test',
}),
).toThrow('Unexpected value for RICH_TEXT_V2 filter');
});
});
});
@@ -0,0 +1,85 @@
import { isMatchingSelectFilter } from '@/utils/filter/utils/isMatchingSelectFilter';
describe('isMatchingSelectFilter', () => {
describe('in', () => {
it('should return true when value is in the list', () => {
expect(
isMatchingSelectFilter({
selectFilter: { in: ['ACTIVE', 'PENDING'] },
value: 'ACTIVE',
}),
).toBe(true);
});
it('should return false when value is not in the list', () => {
expect(
isMatchingSelectFilter({
selectFilter: { in: ['ACTIVE', 'PENDING'] },
value: 'CLOSED',
}),
).toBe(false);
});
});
describe('is', () => {
it('should match NULL check', () => {
expect(
isMatchingSelectFilter({
selectFilter: { is: 'NULL' },
value: null as any,
}),
).toBe(true);
});
it('should match NOT_NULL check', () => {
expect(
isMatchingSelectFilter({
selectFilter: { is: 'NOT_NULL' },
value: 'ACTIVE',
}),
).toBe(true);
});
});
describe('eq', () => {
it('should return true when value equals', () => {
expect(
isMatchingSelectFilter({
selectFilter: { eq: 'ACTIVE' },
value: 'ACTIVE',
}),
).toBe(true);
});
it('should return false when value does not equal', () => {
expect(
isMatchingSelectFilter({
selectFilter: { eq: 'ACTIVE' },
value: 'CLOSED',
}),
).toBe(false);
});
});
describe('neq', () => {
it('should return true when value does not equal', () => {
expect(
isMatchingSelectFilter({
selectFilter: { neq: 'ACTIVE' },
value: 'CLOSED',
}),
).toBe(true);
});
});
describe('default', () => {
it('should throw for unexpected filter', () => {
expect(() =>
isMatchingSelectFilter({
selectFilter: {} as any,
value: 'ACTIVE',
}),
).toThrow('Unexpected value for select filter');
});
});
});
@@ -0,0 +1,19 @@
import { getGenericOperationName } from '@/utils/sentry/getGenericOperationName';
describe('getGenericOperationName', () => {
it('should extract the first word from a PascalCase name', () => {
expect(getGenericOperationName('FindOnePerson')).toBe('Find');
});
it('should extract the first word from another PascalCase name', () => {
expect(getGenericOperationName('AggregateCompanies')).toBe('Aggregate');
});
it('should return undefined for undefined input', () => {
expect(getGenericOperationName(undefined)).toBeUndefined();
});
it('should handle single word', () => {
expect(getGenericOperationName('Create')).toBe('Create');
});
});
@@ -0,0 +1,19 @@
import { getHumanReadableNameFromCode } from '@/utils/sentry/getHumanReadableNameFromCode';
describe('getHumanReadableNameFromCode', () => {
it('should convert snake_case to Title Case', () => {
expect(getHumanReadableNameFromCode('hello_world')).toBe('Hello World');
});
it('should handle single word', () => {
expect(getHumanReadableNameFromCode('hello')).toBe('Hello');
});
it('should handle uppercase code', () => {
expect(getHumanReadableNameFromCode('NOT_FOUND')).toBe('Not Found');
});
it('should handle multiple underscores', () => {
expect(getHumanReadableNameFromCode('a_b_c_d')).toBe('A B C D');
});
});
@@ -0,0 +1,24 @@
import { isPlainObject } from '@/utils/typeguard/isPlainObject';
describe('isPlainObject', () => {
it('should return true for plain objects', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ key: 'value' })).toBe(true);
});
it('should return false for arrays', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject([1, 2, 3])).toBe(false);
});
it('should return false for null', () => {
expect(isPlainObject(null)).toBe(false);
});
it('should return false for primitives', () => {
expect(isPlainObject(42)).toBe(false);
expect(isPlainObject('string')).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
expect(isPlainObject(true)).toBe(false);
});
});
@@ -0,0 +1,22 @@
import { throwIfNotDefined } from '@/utils/typeguard/throwIfNotDefined';
describe('throwIfNotDefined', () => {
it('should not throw for defined values', () => {
expect(() => throwIfNotDefined('hello', 'myVar')).not.toThrow();
expect(() => throwIfNotDefined(0, 'myVar')).not.toThrow();
expect(() => throwIfNotDefined(false, 'myVar')).not.toThrow();
expect(() => throwIfNotDefined('', 'myVar')).not.toThrow();
});
it('should throw for null', () => {
expect(() => throwIfNotDefined(null, 'myVar')).toThrow(
'Value must be defined for variable myVar',
);
});
it('should throw for undefined', () => {
expect(() => throwIfNotDefined(undefined, 'myVar')).toThrow(
'Value must be defined for variable myVar',
);
});
});
@@ -0,0 +1,23 @@
import { getAbsoluteUrl } from '@/utils/url/getAbsoluteUrl';
describe('getAbsoluteUrl', () => {
it('should return https URL as-is', () => {
expect(getAbsoluteUrl('https://example.com')).toBe('https://example.com');
});
it('should return http URL as-is', () => {
expect(getAbsoluteUrl('http://example.com')).toBe('http://example.com');
});
it('should return HTTPS URL as-is', () => {
expect(getAbsoluteUrl('HTTPS://example.com')).toBe('HTTPS://example.com');
});
it('should return HTTP URL as-is', () => {
expect(getAbsoluteUrl('HTTP://example.com')).toBe('HTTP://example.com');
});
it('should prepend https:// to bare domains', () => {
expect(getAbsoluteUrl('example.com')).toBe('https://example.com');
});
});
@@ -0,0 +1,15 @@
import { safeDecodeURIComponent } from '@/utils/url/safeDecodeURIComponent';
describe('safeDecodeURIComponent', () => {
it('should decode valid encoded components', () => {
expect(safeDecodeURIComponent('hello%20world')).toBe('hello world');
});
it('should return malformed input as-is', () => {
expect(safeDecodeURIComponent('%E0%A4%A')).toBe('%E0%A4%A');
});
it('should return plain text as-is', () => {
expect(safeDecodeURIComponent('hello')).toBe('hello');
});
});
@@ -0,0 +1,23 @@
import { isValidVariable } from '@/utils/validation/isValidVariable';
describe('isValidVariable', () => {
it('should return true for valid variable syntax', () => {
expect(isValidVariable('{{step.output}}')).toBe(true);
});
it('should return true for nested variable', () => {
expect(isValidVariable('{{trigger.output.name}}')).toBe(true);
});
it('should return false for string without brackets', () => {
expect(isValidVariable('no brackets')).toBe(false);
});
it('should return false for partial brackets', () => {
expect(isValidVariable('{{incomplete')).toBe(false);
});
it('should return false for empty brackets', () => {
expect(isValidVariable('{{}}')).toBe(false);
});
});
@@ -0,0 +1,20 @@
import { getCountryCodesForCallingCode } from '@/utils/validation/phones-value/getCountryCodesForCallingCode';
describe('getCountryCodesForCallingCode', () => {
it('should return country codes for calling code 1 (US/CA)', () => {
const result = getCountryCodesForCallingCode('1');
expect(result).toContain('US');
expect(result).toContain('CA');
});
it('should handle + prefix', () => {
const result = getCountryCodesForCallingCode('+33');
expect(result).toContain('FR');
});
it('should return empty array for invalid calling code', () => {
expect(getCountryCodesForCallingCode('99999')).toEqual([]);
});
});
@@ -0,0 +1,19 @@
import { isValidCountryCode } from '@/utils/validation/phones-value/isValidCountryCode';
describe('isValidCountryCode', () => {
it('should return true for valid country code US', () => {
expect(isValidCountryCode('US')).toBe(true);
});
it('should return true for valid country code FR', () => {
expect(isValidCountryCode('FR')).toBe(true);
});
it('should return false for invalid country code', () => {
expect(isValidCountryCode('XX')).toBe(false);
});
it('should return false for lowercase', () => {
expect(isValidCountryCode('us')).toBe(false);
});
});