Morph many to one picker (#14155)
This PR follows the multiSelect PR merged previously. It will enable morph relation Many to One to be handled from the table, using a singleSelect picker Main point : I decided to change the singleSelect API to take an array of **objectMetadataName** instead of only one to deal with both our usecases. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -6,19 +6,19 @@ describe('applyDiff', () => {
|
||||
describe('input validation', () => {
|
||||
it('should throw error for non-array diffs', () => {
|
||||
const obj = { test: 'value' };
|
||||
|
||||
|
||||
expect(() => applyDiff(obj, null as any)).toThrow(
|
||||
'Diffs must be an array'
|
||||
'Diffs must be an array',
|
||||
);
|
||||
expect(() => applyDiff(obj, 'invalid' as any)).toThrow(
|
||||
'Diffs must be an array'
|
||||
'Diffs must be an array',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty diffs array', () => {
|
||||
const obj = { test: 'value' };
|
||||
const result = applyDiff(obj, []);
|
||||
|
||||
|
||||
expect(result).toEqual({ test: 'value' });
|
||||
expect(result).not.toBe(obj); // Should return a copy
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe('applyDiff', () => {
|
||||
{ type: 'CREATE', path: [], value: 'test' } as any,
|
||||
{ type: 'CREATE', path: ['test'], value: 'updated' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({ test: 'updated' });
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CREATE', path: ['newProp'], value: 'newValue' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
existing: 'value',
|
||||
@@ -55,7 +55,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CREATE', path: ['level1', 'newProp'], value: 'newValue' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
level1: {
|
||||
@@ -70,7 +70,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CREATE', path: [1], value: 'newElement' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['existing', 'newElement']);
|
||||
});
|
||||
@@ -80,7 +80,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
level1: {
|
||||
@@ -98,7 +98,7 @@ describe('applyDiff', () => {
|
||||
{ type: 'CREATE', path: ['level1', 'level2'], value: {} },
|
||||
{ type: 'CREATE', path: ['level1', 'level2', 'prop'], value: 'deep' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
level1: {
|
||||
@@ -114,9 +114,14 @@ describe('applyDiff', () => {
|
||||
it('should change existing properties', () => {
|
||||
const obj = { prop: 'oldValue' };
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['prop'], oldValue: 'oldValue', value: 'newValue' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['prop'],
|
||||
oldValue: 'oldValue',
|
||||
value: 'newValue',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({ prop: 'newValue' });
|
||||
});
|
||||
@@ -124,9 +129,14 @@ describe('applyDiff', () => {
|
||||
it('should change nested properties', () => {
|
||||
const obj = { level1: { prop: 'oldValue' } };
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['level1', 'prop'], oldValue: 'oldValue', value: 'newValue' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['level1', 'prop'],
|
||||
oldValue: 'oldValue',
|
||||
value: 'newValue',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
level1: { prop: 'newValue' },
|
||||
@@ -138,7 +148,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: [0], oldValue: 'old', value: 'new' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['new', 'values']);
|
||||
});
|
||||
@@ -151,9 +161,14 @@ describe('applyDiff', () => {
|
||||
],
|
||||
};
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['users', 0, 'name'], oldValue: 'John', value: 'Johnny' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['users', 0, 'name'],
|
||||
oldValue: 'John',
|
||||
value: 'Johnny',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
users: [
|
||||
@@ -170,7 +185,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({ keep: 'value' });
|
||||
});
|
||||
@@ -185,7 +200,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: ['level1', 'remove'], oldValue: 'toDelete' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
level1: { keep: 'value' },
|
||||
@@ -197,7 +212,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: [1], oldValue: 'remove' },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['keep1', 'keep2']);
|
||||
});
|
||||
@@ -208,7 +223,7 @@ describe('applyDiff', () => {
|
||||
{ type: 'REMOVE', path: [1], oldValue: 'b' }, // Remove 'b'
|
||||
{ type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd'
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['a', 'c', 'e']);
|
||||
});
|
||||
@@ -220,7 +235,7 @@ describe('applyDiff', () => {
|
||||
{ type: 'REMOVE', path: [2], oldValue: 'c' }, // Remove 'c'
|
||||
{ type: 'REMOVE', path: [4], oldValue: 'e' }, // Remove 'e'
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['b', 'd', 'f']);
|
||||
});
|
||||
@@ -235,7 +250,7 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: ['items', 0, 'tags', 1], oldValue: 'tag2' }, // Remove 'tag2'
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
@@ -250,9 +265,9 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: ['invalid'], oldValue: 'invalid' },
|
||||
];
|
||||
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
'Expected numeric index for array removal, got string'
|
||||
'Expected numeric index for array removal, got string',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -267,15 +282,20 @@ describe('applyDiff', () => {
|
||||
array: ['a', 'b', 'c'],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CREATE', path: ['newProp'], value: 'newValue' },
|
||||
{ type: 'CHANGE', path: ['change'], oldValue: 'oldValue', value: 'newValue' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['change'],
|
||||
oldValue: 'oldValue',
|
||||
value: 'newValue',
|
||||
},
|
||||
{ type: 'REMOVE', path: ['remove'], oldValue: 'toDelete' },
|
||||
{ type: 'REMOVE', path: ['nested', 'array', 1], oldValue: 'b' }, // Remove 'b'
|
||||
{ type: 'CREATE', path: ['nested', 'newArray'], value: [1, 2, 3] },
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
keep: 'value',
|
||||
@@ -296,7 +316,7 @@ describe('applyDiff', () => {
|
||||
{ type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd'
|
||||
{ type: 'REMOVE', path: [3], oldValue: 'd' }, // Remove 'd'
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual(['A', 'c']);
|
||||
});
|
||||
@@ -308,20 +328,25 @@ describe('applyDiff', () => {
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'INVALID' as any, path: ['test'], value: 'newValue' },
|
||||
];
|
||||
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
'Unsupported diff type: INVALID'
|
||||
'Unsupported diff type: INVALID',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error with path information for invalid operations', () => {
|
||||
const obj = { test: 'value' };
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['nonExistent', 'deep', 'path'], oldValue: 'value', value: 'value' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['nonExistent', 'deep', 'path'],
|
||||
oldValue: 'value',
|
||||
value: 'value',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
'Failed to apply diff at path nonExistent.deep.path'
|
||||
'Failed to apply diff at path nonExistent.deep.path',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -335,7 +360,7 @@ describe('applyDiff', () => {
|
||||
];
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
"Refusing to set forbidden property key '__proto__' on object (prototype pollution protection)"
|
||||
"Refusing to set forbidden property key '__proto__' on object (prototype pollution protection)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -344,20 +369,20 @@ describe('applyDiff', () => {
|
||||
|
||||
const unicodeBypassAttempts = [
|
||||
// __proto__ with Unicode escapes
|
||||
'__\u0070roto__', // \u0070 = 'p'
|
||||
'__\u{70}roto__', // ES6 syntax
|
||||
'__pr\u006fto__', // \u006f = 'o'
|
||||
'__\u0070roto__', // \u0070 = 'p'
|
||||
'__\u{70}roto__', // ES6 syntax
|
||||
'__pr\u006fto__', // \u006f = 'o'
|
||||
'__proto\u005f\u005f', // \u005f = '_'
|
||||
|
||||
// constructor with Unicode escapes
|
||||
// constructor with Unicode escapes
|
||||
'construc\u0074or', // \u0074 = 't'
|
||||
'constr\u0075ctor', // \u0075 = 'u'
|
||||
'\u0063onstructor', // \u0063 = 'c'
|
||||
|
||||
// prototype with Unicode escapes
|
||||
'proto\u0074ype', // \u0074 = 't'
|
||||
'prototy\u0070e', // \u0070 = 'p'
|
||||
'\u0070rototype', // \u0070 = 'p'
|
||||
'proto\u0074ype', // \u0074 = 't'
|
||||
'prototy\u0070e', // \u0070 = 'p'
|
||||
'\u0070rototype', // \u0070 = 'p'
|
||||
];
|
||||
|
||||
unicodeBypassAttempts.forEach((maliciousKey) => {
|
||||
@@ -366,7 +391,9 @@ describe('applyDiff', () => {
|
||||
];
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
new RegExp(`Refusing to set forbidden property key.*prototype pollution protection`)
|
||||
new RegExp(
|
||||
`Refusing to set forbidden property key.*prototype pollution protection`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -375,11 +402,16 @@ describe('applyDiff', () => {
|
||||
const obj = { safe: 'value' };
|
||||
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['constructor'], oldValue: 'old', value: 'malicious' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['constructor'],
|
||||
oldValue: 'old',
|
||||
value: 'malicious',
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => applyDiff(obj, diffs)).toThrow(
|
||||
"Refusing to set forbidden property key 'constructor' on object (prototype pollution protection)"
|
||||
"Refusing to set forbidden property key 'constructor' on object (prototype pollution protection)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -414,36 +446,51 @@ describe('applyDiff', () => {
|
||||
it('should not modify the original object', () => {
|
||||
const obj = { prop: 'value', nested: { deep: 'value' } };
|
||||
const originalObj = JSON.parse(JSON.stringify(obj));
|
||||
|
||||
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['prop'], oldValue: 'value', value: 'newValue' },
|
||||
{ type: 'CHANGE', path: ['nested', 'deep'], oldValue: 'value', value: 'newDeepValue' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['prop'],
|
||||
oldValue: 'value',
|
||||
value: 'newValue',
|
||||
},
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['nested', 'deep'],
|
||||
oldValue: 'value',
|
||||
value: 'newDeepValue',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
applyDiff(obj, diffs);
|
||||
|
||||
|
||||
expect(obj).toEqual(originalObj);
|
||||
});
|
||||
|
||||
it('should not modify the original array', () => {
|
||||
const obj = ['a', 'b', 'c'];
|
||||
const originalObj = [...obj];
|
||||
|
||||
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'REMOVE', path: [1], oldValue: 'b' },
|
||||
];
|
||||
|
||||
|
||||
applyDiff(obj, diffs);
|
||||
|
||||
|
||||
expect(obj).toEqual(originalObj);
|
||||
});
|
||||
|
||||
it('should handle frozen objects', () => {
|
||||
const obj = Object.freeze({ prop: 'value' });
|
||||
const diffs: Difference[] = [
|
||||
{ type: 'CHANGE', path: ['prop'], oldValue: 'value', value: 'newValue' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['prop'],
|
||||
oldValue: 'value',
|
||||
value: 'newValue',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({ prop: 'newValue' });
|
||||
expect(obj.prop).toBe('value'); // Original unchanged
|
||||
@@ -470,22 +517,36 @@ describe('applyDiff', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
const diffs: Difference[] = [
|
||||
// Update trigger settings
|
||||
{ type: 'CHANGE', path: ['trigger', 'settings', 'table'], oldValue: 'users', value: 'contacts' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['trigger', 'settings', 'table'],
|
||||
oldValue: 'users',
|
||||
value: 'contacts',
|
||||
},
|
||||
// Remove first step
|
||||
{ type: 'REMOVE', path: ['steps', 0], oldValue: '1' },
|
||||
// Update remaining step
|
||||
{ type: 'CHANGE', path: ['steps', 1, 'settings', 'to'], oldValue: 'test@example.com', value: 'new@example.com' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['steps', 1, 'settings', 'to'],
|
||||
oldValue: 'test@example.com',
|
||||
value: 'new@example.com',
|
||||
},
|
||||
// Add new step
|
||||
{ type: 'CREATE', path: ['steps', 2], value: {
|
||||
id: '3',
|
||||
type: 'WEBHOOK',
|
||||
settings: { url: 'https://api.example.com/webhook' },
|
||||
}},
|
||||
{
|
||||
type: 'CREATE',
|
||||
path: ['steps', 2],
|
||||
value: {
|
||||
id: '3',
|
||||
type: 'WEBHOOK',
|
||||
settings: { url: 'https://api.example.com/webhook' },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
trigger: {
|
||||
@@ -517,18 +578,27 @@ describe('applyDiff', () => {
|
||||
{ id: 5, name: 'Item 5' },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
const diffs: Difference[] = [
|
||||
// Remove items at indices 1 and 3 (Item 2 and Item 4)
|
||||
{ type: 'REMOVE', path: ['items', 1], oldValue: 'Item 2' },
|
||||
{ type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' },
|
||||
{ type: 'REMOVE', path: ['items', 3], oldValue: 'Item 4' },
|
||||
// Update remaining item
|
||||
{ type: 'CHANGE', path: ['items', 0, 'name'], oldValue: 'Item 1', value: 'Updated Item 1' },
|
||||
{
|
||||
type: 'CHANGE',
|
||||
path: ['items', 0, 'name'],
|
||||
oldValue: 'Item 1',
|
||||
value: 'Updated Item 1',
|
||||
},
|
||||
// Add new item
|
||||
{ type: 'CREATE', path: ['items', 5], value: { id: 6, name: 'New Item' } },
|
||||
{
|
||||
type: 'CREATE',
|
||||
path: ['items', 5],
|
||||
value: { id: 6, name: 'New Item' },
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const result = applyDiff(obj, diffs);
|
||||
expect(result).toEqual({
|
||||
items: [
|
||||
|
||||
@@ -3,36 +3,40 @@ import { findOrThrow } from '@/utils';
|
||||
describe('findOrThrow', () => {
|
||||
it('returns the element when predicate matches', () => {
|
||||
const arr = [1, 2, 3, 4];
|
||||
const result = findOrThrow(arr, x => x === 3);
|
||||
const result = findOrThrow(arr, (x) => x === 3);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
it('throws default error when no element matches', () => {
|
||||
const arr = [1, 2, 3];
|
||||
expect(() => findOrThrow(arr, x => x === 5)).toThrow(new Error('Element not found'));
|
||||
expect(() => findOrThrow(arr, (x) => x === 5)).toThrow(
|
||||
new Error('Element not found'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws custom error when provided', () => {
|
||||
const arr = ['a', 'b', 'c'];
|
||||
const customError = new Error('Custom error!');
|
||||
expect(() => findOrThrow(arr, x => x === 'z', customError)).toThrow(customError);
|
||||
expect(() => findOrThrow(arr, (x) => x === 'z', customError)).toThrow(
|
||||
customError,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the first matching element when multiple match', () => {
|
||||
const arr = [2, 4, 6, 8];
|
||||
const result = findOrThrow(arr, x => x % 2 === 0);
|
||||
const result = findOrThrow(arr, (x) => x % 2 === 0);
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it('works with object arrays', () => {
|
||||
const arr = [{id: 1}, {id: 2}, {id: 3}];
|
||||
const result = findOrThrow(arr, obj => obj.id === 2);
|
||||
expect(result).toEqual({id: 2});
|
||||
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||
const result = findOrThrow(arr, (obj) => obj.id === 2);
|
||||
expect(result).toEqual({ id: 2 });
|
||||
});
|
||||
|
||||
it('works with primitive arrays', () => {
|
||||
const arr = ['apple', 'banana', 'cherry'];
|
||||
const result = findOrThrow(arr, fruit => fruit.startsWith('b'));
|
||||
const result = findOrThrow(arr, (fruit) => fruit.startsWith('b'));
|
||||
expect(result).toBe('banana');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNonEmptyArray } from "@/utils/array/isNonEmptyArray";
|
||||
import { isNonEmptyArray } from '@/utils/array/isNonEmptyArray';
|
||||
|
||||
describe('isNonEmptyArray', () => {
|
||||
it('should return true for a non empty array', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseJson } from "@/utils/parseJson";
|
||||
import { parseJson } from '@/utils/parseJson';
|
||||
|
||||
describe('parseJson', () => {
|
||||
it('if value is null', () => {
|
||||
|
||||
@@ -419,4 +419,4 @@ describe('safeParseRelativeDateFilterValue', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,9 @@ export const applyDiff = <T>(obj: T, diffs: Difference[]): T => {
|
||||
try {
|
||||
applyDiffToPath(mutableObj as MutableData, diff, arrayDeletionQueue);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to apply diff at path ${diff.path.join('.')}: ${error}`);
|
||||
throw new Error(
|
||||
`Failed to apply diff at path ${diff.path.join('.')}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,82 +48,103 @@ export const applyDiff = <T>(obj: T, diffs: Difference[]): T => {
|
||||
const applyDiffToPath = (
|
||||
obj: MutableData,
|
||||
diff: Difference,
|
||||
arrayDeletionQueue: ArrayDeletionFn[]
|
||||
arrayDeletionQueue: ArrayDeletionFn[],
|
||||
) => {
|
||||
const { path, type } = diff;
|
||||
const value = 'value' in diff ? diff.value : undefined;
|
||||
const pathLength = path.length;
|
||||
const lastPathElement = path[pathLength - 1];
|
||||
|
||||
|
||||
const parentContainer = navigateToParent(obj, path);
|
||||
|
||||
|
||||
switch (type) {
|
||||
case 'CREATE':
|
||||
case 'CHANGE':
|
||||
setValueAtPath(parentContainer, lastPathElement, value);
|
||||
break;
|
||||
|
||||
|
||||
case 'REMOVE':
|
||||
handleRemoval(obj, path, parentContainer, lastPathElement, arrayDeletionQueue);
|
||||
handleRemoval(
|
||||
obj,
|
||||
path,
|
||||
parentContainer,
|
||||
lastPathElement,
|
||||
arrayDeletionQueue,
|
||||
);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported diff type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToParent = (obj: MutableData, path: (string | number)[]): MutableData => {
|
||||
const navigateToParent = (
|
||||
obj: MutableData,
|
||||
path: (string | number)[],
|
||||
): MutableData => {
|
||||
let current = obj;
|
||||
|
||||
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const pathElement = path[i];
|
||||
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
throw new Error(`Cannot traverse path: found null/undefined at element ${i}`);
|
||||
throw new Error(
|
||||
`Cannot traverse path: found null/undefined at element ${i}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (isNumber(pathElement) && !Array.isArray(current)) {
|
||||
throw new Error(`Expected array at path element ${i}, got ${typeof current}`);
|
||||
throw new Error(
|
||||
`Expected array at path element ${i}, got ${typeof current}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (isString(pathElement) && Array.isArray(current)) {
|
||||
throw new Error(`Expected object at path element ${i}, got array`);
|
||||
}
|
||||
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
current = current[pathElement as number] as MutableData;
|
||||
} else {
|
||||
current = (current as ObjectType)[pathElement as string] as MutableData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const setValueAtPath = (
|
||||
container: MutableData,
|
||||
pathElement: string | number,
|
||||
value: unknown
|
||||
value: unknown,
|
||||
): void => {
|
||||
if (Array.isArray(container)) {
|
||||
if (!isNumber(pathElement)) {
|
||||
throw new Error(`Expected numeric index for array, got ${typeof pathElement}`);
|
||||
}
|
||||
|
||||
try {
|
||||
container[pathElement] = value;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot set array element at index ${pathElement}: ${error}. Array may be non-extensible.`);
|
||||
}
|
||||
} else if (isObject(container)) {
|
||||
if (FORBIDDEN_OBJECT_KEYS.includes(pathElement as string)) {
|
||||
throw new Error(`Refusing to set forbidden property key '${pathElement}' on object (prototype pollution protection)`);
|
||||
throw new Error(
|
||||
`Expected numeric index for array, got ${typeof pathElement}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
container[pathElement] = value;
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot set property '${String(pathElement)}': ${error}. Object may be non-extensible.`);
|
||||
throw new Error(
|
||||
`Cannot set array element at index ${pathElement}: ${error}. Array may be non-extensible.`,
|
||||
);
|
||||
}
|
||||
} else if (isObject(container)) {
|
||||
if (FORBIDDEN_OBJECT_KEYS.includes(pathElement as string)) {
|
||||
throw new Error(
|
||||
`Refusing to set forbidden property key '${pathElement}' on object (prototype pollution protection)`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
container[pathElement] = value;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Cannot set property '${String(pathElement)}': ${error}. Object may be non-extensible.`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Expected object or array, got ${typeof container}`);
|
||||
@@ -133,10 +156,16 @@ const handleRemoval = (
|
||||
fullPath: (string | number)[],
|
||||
parentContainer: MutableData,
|
||||
lastPathElement: string | number,
|
||||
arrayDeletionQueue: ArrayDeletionFn[]
|
||||
arrayDeletionQueue: ArrayDeletionFn[],
|
||||
): void => {
|
||||
if (Array.isArray(parentContainer)) {
|
||||
handleArrayRemoval(rootObj, fullPath, parentContainer, lastPathElement, arrayDeletionQueue);
|
||||
handleArrayRemoval(
|
||||
rootObj,
|
||||
fullPath,
|
||||
parentContainer,
|
||||
lastPathElement,
|
||||
arrayDeletionQueue,
|
||||
);
|
||||
} else {
|
||||
handleObjectRemoval(parentContainer as ObjectType, lastPathElement);
|
||||
}
|
||||
@@ -148,14 +177,16 @@ const handleArrayRemoval = (
|
||||
fullPath: (string | number)[],
|
||||
parentArray: ArrayType,
|
||||
index: string | number,
|
||||
arrayDeletionQueue: ArrayDeletionFn[]
|
||||
arrayDeletionQueue: ArrayDeletionFn[],
|
||||
): void => {
|
||||
if (typeof index !== 'number') {
|
||||
throw new Error(`Expected numeric index for array removal, got ${typeof index}`);
|
||||
throw new Error(
|
||||
`Expected numeric index for array removal, got ${typeof index}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
parentArray[index] = REMOVE_SYMBOL;
|
||||
|
||||
|
||||
arrayDeletionQueue.push(() => {
|
||||
if (fullPath.length === 1) {
|
||||
if (Array.isArray(rootObj)) {
|
||||
@@ -169,7 +200,7 @@ const handleArrayRemoval = (
|
||||
|
||||
const handleObjectRemoval = (
|
||||
parentObject: ObjectType,
|
||||
key: string | number
|
||||
key: string | number,
|
||||
): void => {
|
||||
if (FORBIDDEN_OBJECT_KEYS.includes(key as string)) {
|
||||
return;
|
||||
@@ -180,13 +211,13 @@ const handleObjectRemoval = (
|
||||
|
||||
const filterRemoveSymbols = (array: ArrayType): void => {
|
||||
const indicesToRemove: number[] = [];
|
||||
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] === REMOVE_SYMBOL) {
|
||||
indicesToRemove.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (let i = indicesToRemove.length - 1; i >= 0; i--) {
|
||||
array.splice(indicesToRemove[i], 1);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const filterOutByProperty = <T, K extends keyof T>(property: K, valueToExclude: T[K] | null | undefined) => {
|
||||
export const filterOutByProperty = <T, K extends keyof T>(
|
||||
property: K,
|
||||
valueToExclude: T[K] | null | undefined,
|
||||
) => {
|
||||
return (itemToFilter: T) => {
|
||||
return itemToFilter[property] !== valueToExclude
|
||||
}
|
||||
}
|
||||
return itemToFilter[property] !== valueToExclude;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const findById = <T extends { id: string }>(idToMatch: string) => {
|
||||
return (itemToFind: T) => {
|
||||
return itemToFind.id === idToMatch
|
||||
}
|
||||
}
|
||||
return itemToFind.id === idToMatch;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const findByProperty = <T, K extends keyof T>(property: K, valueToMatch: T[K] | null | undefined) => {
|
||||
export const findByProperty = <T, K extends keyof T>(
|
||||
property: K,
|
||||
valueToMatch: T[K] | null | undefined,
|
||||
) => {
|
||||
return (itemToFind: T) => {
|
||||
return itemToFind[property] === valueToMatch
|
||||
}
|
||||
}
|
||||
return itemToFind[property] === valueToMatch;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,11 +3,11 @@ import { assertIsDefinedOrThrow } from '@/utils';
|
||||
export const findOrThrow = <T>(
|
||||
array: T[],
|
||||
predicate: (value: T) => boolean,
|
||||
error: Error = new Error('Element not found'),
|
||||
error: Error = new Error('Element not found'),
|
||||
): T => {
|
||||
const result = array.find(predicate);
|
||||
|
||||
assertIsDefinedOrThrow(result, error)
|
||||
assertIsDefinedOrThrow(result, error);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { isNumberOrNaN } from "@sniptt/guards";
|
||||
import { isNumberOrNaN } from '@sniptt/guards';
|
||||
|
||||
export const sumByProperty = <T, K extends keyof T>(property: K) => {
|
||||
return (accumulator: number, nextItem: T) => {
|
||||
if(typeof accumulator !== "number") {
|
||||
if (typeof accumulator !== 'number') {
|
||||
accumulator = 0;
|
||||
}
|
||||
|
||||
if(!isNumberOrNaN(nextItem[property])) {
|
||||
if (!isNumberOrNaN(nextItem[property])) {
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
accumulator += nextItem[property];
|
||||
|
||||
return accumulator
|
||||
}
|
||||
}
|
||||
return accumulator;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CustomError } from '@/utils/errors';
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
enum RelationType {
|
||||
MANY_TO_ONE = 'MANY_TO_ONE',
|
||||
ONE_TO_MANY = 'ONE_TO_MANY',
|
||||
}
|
||||
|
||||
type ComputeMorphRelationFieldNameArgs = {
|
||||
fieldName: string;
|
||||
relationType: RelationType;
|
||||
targetObjectMetadataNameSingular: string;
|
||||
targetObjectMetadataNamePlural: string;
|
||||
};
|
||||
|
||||
export const computeMorphRelationFieldName = ({
|
||||
fieldName,
|
||||
relationType,
|
||||
targetObjectMetadataNameSingular: nameSingular,
|
||||
targetObjectMetadataNamePlural: namePlural,
|
||||
}: ComputeMorphRelationFieldNameArgs): string => {
|
||||
if (relationType === RelationType.MANY_TO_ONE) {
|
||||
return `${fieldName}${capitalize(nameSingular)}`;
|
||||
}
|
||||
|
||||
if (relationType === RelationType.ONE_TO_MANY) {
|
||||
return `${fieldName}${capitalize(namePlural)}`;
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
`Invalid relation type (${relationType}) for field ${fieldName} on ${nameSingular}`,
|
||||
'INVALID_RELATION_TYPE_FOR_COMPUTE_MORPH_RELATION_FIELD_NAME',
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
import { type FieldMetadataType, type FilterableAndTSVectorFieldType, type ViewFilterOperand } from '@/types';
|
||||
import {
|
||||
type FieldMetadataType,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
type ViewFilterOperand,
|
||||
} from '@/types';
|
||||
import { isEmptinessOperand } from '@/utils/filter/isEmptinessOperand';
|
||||
import { getFilterTypeFromFieldType } from '@/utils/filter/utils/getFilterTypeFromFieldType';
|
||||
|
||||
@@ -7,7 +11,7 @@ export const checkIfShouldComputeEmptinessFilter = ({
|
||||
correspondingFieldMetadataItem,
|
||||
}: {
|
||||
recordFilterOperand: ViewFilterOperand;
|
||||
correspondingFieldMetadataItem: {type: FieldMetadataType};
|
||||
correspondingFieldMetadataItem: { type: FieldMetadataType };
|
||||
}) => {
|
||||
const isAnEmptinessOperand = isEmptinessOperand(recordFilterOperand);
|
||||
|
||||
|
||||
+4
-1
@@ -16,7 +16,10 @@ export const computeGqlOperationFilterForEmails = ({
|
||||
subFieldName,
|
||||
}: {
|
||||
recordFilter: RecordFilter;
|
||||
correspondingFieldMetadataItem: Pick<PartialFieldMetadataItem, 'name' | 'type'>;
|
||||
correspondingFieldMetadataItem: Pick<
|
||||
PartialFieldMetadataItem,
|
||||
'name' | 'type'
|
||||
>;
|
||||
subFieldName: CompositeFieldSubFieldName | null | undefined;
|
||||
}): RecordGqlOperationFilter => {
|
||||
const isSubFieldFilter = isNonEmptyString(subFieldName);
|
||||
|
||||
+10
-2
@@ -1,4 +1,9 @@
|
||||
import { ViewFilterOperand as RecordFilterOperand, type CompositeFieldSubFieldName, type LinksFilter, type PartialFieldMetadataItem } from '@/types';
|
||||
import {
|
||||
ViewFilterOperand as RecordFilterOperand,
|
||||
type CompositeFieldSubFieldName,
|
||||
type LinksFilter,
|
||||
type PartialFieldMetadataItem,
|
||||
} from '@/types';
|
||||
import { CustomError } from '@/utils/errors';
|
||||
import { type RecordFilter } from '@/utils/filter/turnRecordFilterGroupIntoGqlOperationFilter';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
@@ -9,7 +14,10 @@ export const computeGqlOperationFilterForLinks = ({
|
||||
subFieldName,
|
||||
}: {
|
||||
recordFilter: RecordFilter;
|
||||
correspondingFieldMetadataItem: Pick<PartialFieldMetadataItem, 'name' | 'type'>
|
||||
correspondingFieldMetadataItem: Pick<
|
||||
PartialFieldMetadataItem,
|
||||
'name' | 'type'
|
||||
>;
|
||||
subFieldName: CompositeFieldSubFieldName | null | undefined;
|
||||
}) => {
|
||||
const isSubFieldFilter = isNonEmptyString(subFieldName);
|
||||
|
||||
@@ -13,7 +13,10 @@ export const computeEmptyGqlOperationFilterForEmails = ({
|
||||
correspondingFieldMetadataItem,
|
||||
}: {
|
||||
recordFilter: RecordFilter;
|
||||
correspondingFieldMetadataItem: Pick<PartialFieldMetadataItem, 'name' | 'type'>;
|
||||
correspondingFieldMetadataItem: Pick<
|
||||
PartialFieldMetadataItem,
|
||||
'name' | 'type'
|
||||
>;
|
||||
}): RecordGqlOperationFilter => {
|
||||
const subFieldName = recordFilter.subFieldName;
|
||||
const isSubFieldFilter = isNonEmptyString(subFieldName);
|
||||
@@ -53,7 +56,7 @@ export const computeEmptyGqlOperationFilterForEmails = ({
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new CustomError(
|
||||
throw new CustomError(
|
||||
`Unknown subfield name ${subFieldName}`,
|
||||
'UNKNOWN_SUBFIELD_NAME',
|
||||
);
|
||||
|
||||
@@ -69,7 +69,7 @@ export const computeEmptyGqlOperationFilterForLinks = ({
|
||||
};
|
||||
}
|
||||
default: {
|
||||
throw new CustomError(
|
||||
throw new CustomError(
|
||||
`Unknown subfield name ${subFieldName}`,
|
||||
'UNKNOWN_SUBFIELD_NAME',
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
type PartialFieldMetadataItem,
|
||||
type RecordFilterValueDependencies,
|
||||
@@ -46,8 +45,7 @@ export const computeRecordGqlOperationFilter = ({
|
||||
fields,
|
||||
recordFilterGroups,
|
||||
currentRecordFilterGroupId: outermostFilterGroupId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const recordGqlOperationFilters = [
|
||||
...regularRecordGqlOperationFilter,
|
||||
|
||||
@@ -19,5 +19,3 @@ export * from './utils/resolveDateViewFilterValue';
|
||||
export * from './utils/validation-schemas/arrayOfStringsOrVariablesSchema';
|
||||
export * from './utils/validation-schemas/arrayOfUuidsOrVariablesSchema';
|
||||
export * from './utils/validation-schemas/jsonRelationFilterValueSchema';
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ViewFilterOperand as RecordFilterOperand } from '@/types';
|
||||
|
||||
export const isEmptinessOperand = (operand: RecordFilterOperand): boolean => {
|
||||
return [RecordFilterOperand.IS_EMPTY, RecordFilterOperand.IS_NOT_EMPTY].includes(
|
||||
operand,
|
||||
);
|
||||
return [
|
||||
RecordFilterOperand.IS_EMPTY,
|
||||
RecordFilterOperand.IS_NOT_EMPTY,
|
||||
].includes(operand);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { CURRENCY_CODE_LABELS } from '@/constants';
|
||||
import { type CurrencyCode } from '@/constants/CurrencyCode';
|
||||
import { FieldMetadataType, ViewFilterOperand, type PartialFieldMetadataItem, type RecordGqlOperationFilter } from '@/types';
|
||||
import { filterSelectOptionsOfFieldMetadataItem, type RecordFilter } from '@/utils';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
ViewFilterOperand,
|
||||
type PartialFieldMetadataItem,
|
||||
type RecordGqlOperationFilter,
|
||||
} from '@/types';
|
||||
import {
|
||||
filterSelectOptionsOfFieldMetadataItem,
|
||||
type RecordFilter,
|
||||
} from '@/utils';
|
||||
import { isNonEmptyArray } from '@/utils/array/isNonEmptyArray';
|
||||
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
import { createAnyFieldRecordFilterBaseProperties } from '@/utils/filter/utils/createAnyFieldRecordFilterBaseProperties';
|
||||
@@ -24,7 +32,6 @@ export const turnAnyFieldFilterIntoRecordGqlFilter = ({
|
||||
filterValue: string;
|
||||
fields: PartialFieldMetadataItem[];
|
||||
}) => {
|
||||
|
||||
const anyFieldRecordFilters: RecordFilter[] = [];
|
||||
|
||||
const isFilterValueANumber = z.coerce.number().safeParse(filterValue).success;
|
||||
|
||||
+16
-8
@@ -1,4 +1,12 @@
|
||||
import { type CompositeFieldSubFieldName, type FilterableAndTSVectorFieldType, type PartialFieldMetadataItem, RecordFilterGroupLogicalOperator, type RecordFilterValueDependencies, type RecordGqlOperationFilter, type ViewFilterOperand } from '@/types';
|
||||
import {
|
||||
type CompositeFieldSubFieldName,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
type PartialFieldMetadataItem,
|
||||
RecordFilterGroupLogicalOperator,
|
||||
type RecordFilterValueDependencies,
|
||||
type RecordGqlOperationFilter,
|
||||
type ViewFilterOperand,
|
||||
} from '@/types';
|
||||
|
||||
import { isDefined } from '@/utils';
|
||||
import { turnRecordFilterIntoRecordGqlOperationFilter } from '@/utils/filter/turnRecordFilterIntoGqlOperationFilter';
|
||||
@@ -17,7 +25,7 @@ export type RecordFilterGroup = {
|
||||
id: string;
|
||||
parentRecordFilterGroupId?: string | null;
|
||||
logicalOperator: RecordFilterGroupLogicalOperator;
|
||||
}
|
||||
};
|
||||
|
||||
export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
filterValueDependencies,
|
||||
@@ -26,11 +34,11 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
recordFilterGroups,
|
||||
currentRecordFilterGroupId,
|
||||
}: {
|
||||
filterValueDependencies: RecordFilterValueDependencies,
|
||||
filters: RecordFilter[],
|
||||
fields: PartialFieldMetadataItem[],
|
||||
recordFilterGroups: RecordFilterGroup[],
|
||||
currentRecordFilterGroupId?: string,
|
||||
filterValueDependencies: RecordFilterValueDependencies;
|
||||
filters: RecordFilter[];
|
||||
fields: PartialFieldMetadataItem[];
|
||||
recordFilterGroups: RecordFilterGroup[];
|
||||
currentRecordFilterGroupId?: string;
|
||||
}): RecordGqlOperationFilter | undefined => {
|
||||
const currentRecordFilterGroup = recordFilterGroups.find(
|
||||
(recordFilterGroup) => recordFilterGroup.id === currentRecordFilterGroupId,
|
||||
@@ -45,7 +53,7 @@ export const turnRecordFilterGroupsIntoGqlOperationFilter = ({
|
||||
);
|
||||
|
||||
const groupRecordGqlOperationFilters = recordFiltersInGroup
|
||||
.map((recordFilter) =>
|
||||
.map((recordFilter) =>
|
||||
turnRecordFilterIntoRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
recordFilter: recordFilter,
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
convertGreaterThanOrEqualRatingToArrayOfRatingValues,
|
||||
convertLessThanOrEqualRatingToArrayOfRatingValues,
|
||||
convertRatingToRatingValue,
|
||||
generateILikeFiltersForCompositeFields, getEmptyRecordGqlOperationFilter,
|
||||
generateILikeFiltersForCompositeFields,
|
||||
getEmptyRecordGqlOperationFilter,
|
||||
isExpectedSubFieldName,
|
||||
} from '@/utils/filter';
|
||||
|
||||
@@ -37,7 +38,14 @@ import { resolveDateViewFilterValue } from '@/utils/filter/utils/resolveDateView
|
||||
import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { checkIfShouldComputeEmptinessFilter, checkIfShouldSkipFiltering, CustomError, getFilterTypeFromFieldType, isDefined, type RecordFilter } from '@/utils';
|
||||
import {
|
||||
checkIfShouldComputeEmptinessFilter,
|
||||
checkIfShouldSkipFiltering,
|
||||
CustomError,
|
||||
getFilterTypeFromFieldType,
|
||||
isDefined,
|
||||
type RecordFilter,
|
||||
} from '@/utils';
|
||||
import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema';
|
||||
import { arrayOfUuidOrVariableSchema } from '@/utils/filter/utils/validation-schemas/arrayOfUuidsOrVariablesSchema';
|
||||
import { jsonRelationFilterValueSchema } from '@/utils/filter/utils/validation-schemas/jsonRelationFilterValueSchema';
|
||||
@@ -47,7 +55,7 @@ type FieldShared = {
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
|
||||
type TurnRecordFilterIntoRecordGqlOperationFilterParams = {
|
||||
filterValueDependencies?: RecordFilterValueDependencies;
|
||||
@@ -56,7 +64,6 @@ type TurnRecordFilterIntoRecordGqlOperationFilterParams = {
|
||||
recordIdsForUuid?: string[];
|
||||
};
|
||||
|
||||
|
||||
export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
recordFilter,
|
||||
fieldMetadataItems,
|
||||
@@ -337,7 +344,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
]
|
||||
: selectedRecordIds;
|
||||
|
||||
if (!isDefined (recordIds) || recordIds.length === 0) return;
|
||||
if (!isDefined(recordIds) || recordIds.length === 0) return;
|
||||
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
@@ -347,7 +354,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
} as RelationFilter,
|
||||
};
|
||||
case RecordFilterOperand.IS_NOT: {
|
||||
if (!isDefined (recordIds) || recordIds.length === 0) return;
|
||||
if (!isDefined(recordIds) || recordIds.length === 0) return;
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
@@ -1201,7 +1208,7 @@ export const turnRecordFilterIntoRecordGqlOperationFilter = ({
|
||||
case 'UUID': {
|
||||
const recordIds = recordIdsForUuid;
|
||||
|
||||
if (!isDefined (recordIds) || recordIds.length === 0) return;
|
||||
if (!isDefined(recordIds) || recordIds.length === 0) return;
|
||||
|
||||
switch (recordFilter.operand) {
|
||||
case RecordFilterOperand.IS:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { arrayOfStringsOrVariablesSchema } from "@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema";
|
||||
import { arrayOfStringsOrVariablesSchema } from '@/utils/filter/utils/validation-schemas/arrayOfStringsOrVariablesSchema';
|
||||
|
||||
describe('arrayOfStringsOrVariablesSchema', () => {
|
||||
describe('Empty value handling', () => {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { generateILikeFiltersForCompositeFields } from "@/utils/filter/utils/generateILikeFiltersForCompositeFields";
|
||||
import { generateILikeFiltersForCompositeFields } from '@/utils/filter/utils/generateILikeFiltersForCompositeFields';
|
||||
|
||||
describe('generateILikeFiltersForCompositeFields', () => {
|
||||
it('should format composite filters for simple filter string', () => {
|
||||
|
||||
+4
-8
@@ -1,7 +1,6 @@
|
||||
import { FieldMetadataType, type PartialFieldMetadataItem } from "@/types";
|
||||
import { turnAnyFieldFilterIntoRecordGqlFilter } from "@/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter";
|
||||
import { filterSelectOptionsOfFieldMetadataItem } from "@/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem";
|
||||
|
||||
import { FieldMetadataType, type PartialFieldMetadataItem } from '@/types';
|
||||
import { turnAnyFieldFilterIntoRecordGqlFilter } from '@/utils/filter/turnAnyFieldFilterIntoRecordGqlFilter';
|
||||
import { filterSelectOptionsOfFieldMetadataItem } from '@/utils/filter/utils/filterSelectOptionsOfFieldMetadataItem';
|
||||
|
||||
const baseFieldMetadataItem: PartialFieldMetadataItem = {
|
||||
id: 'base-field-metadata-item-id',
|
||||
@@ -303,7 +302,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => {
|
||||
const result = turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
filterValue,
|
||||
fields: [arrayFieldMetadataItem],
|
||||
|
||||
});
|
||||
|
||||
expect(result.recordGqlOperationFilter.or).toContainEqual({
|
||||
@@ -351,7 +349,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => {
|
||||
const result = turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
filterValue,
|
||||
fields: [phonesFieldMetadataItem],
|
||||
|
||||
});
|
||||
|
||||
expect(result.recordGqlOperationFilter.or).toContainEqual({
|
||||
@@ -388,7 +385,7 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => {
|
||||
|
||||
const result = turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
filterValue,
|
||||
fields: [numberFieldMetadataItem],
|
||||
fields: [numberFieldMetadataItem],
|
||||
});
|
||||
|
||||
expect(result.recordGqlOperationFilter.or).toContainEqual({
|
||||
@@ -417,7 +414,6 @@ describe('turnAnyFieldFilterIntoRecordGqlFilter', () => {
|
||||
const result = turnAnyFieldFilterIntoRecordGqlFilter({
|
||||
filterValue,
|
||||
fields: [currencyFieldMetadataItem],
|
||||
|
||||
});
|
||||
|
||||
expect(result.recordGqlOperationFilter.or).toContainEqual({
|
||||
|
||||
+6
-3
@@ -4,12 +4,15 @@ const operandMapping: Record<string, ViewFilterOperand> = {
|
||||
[ViewFilterOperandDeprecated.Is]: ViewFilterOperand.IS,
|
||||
[ViewFilterOperandDeprecated.IsNotNull]: ViewFilterOperand.IS_NOT_NULL,
|
||||
[ViewFilterOperandDeprecated.IsNot]: ViewFilterOperand.IS_NOT,
|
||||
[ViewFilterOperandDeprecated.LessThanOrEqual]: ViewFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
[ViewFilterOperandDeprecated.GreaterThanOrEqual]: ViewFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
[ViewFilterOperandDeprecated.LessThanOrEqual]:
|
||||
ViewFilterOperand.LESS_THAN_OR_EQUAL,
|
||||
[ViewFilterOperandDeprecated.GreaterThanOrEqual]:
|
||||
ViewFilterOperand.GREATER_THAN_OR_EQUAL,
|
||||
[ViewFilterOperandDeprecated.IsBefore]: ViewFilterOperand.IS_BEFORE,
|
||||
[ViewFilterOperandDeprecated.IsAfter]: ViewFilterOperand.IS_AFTER,
|
||||
[ViewFilterOperandDeprecated.Contains]: ViewFilterOperand.CONTAINS,
|
||||
[ViewFilterOperandDeprecated.DoesNotContain]: ViewFilterOperand.DOES_NOT_CONTAIN,
|
||||
[ViewFilterOperandDeprecated.DoesNotContain]:
|
||||
ViewFilterOperand.DOES_NOT_CONTAIN,
|
||||
[ViewFilterOperandDeprecated.IsEmpty]: ViewFilterOperand.IS_EMPTY,
|
||||
[ViewFilterOperandDeprecated.IsNotEmpty]: ViewFilterOperand.IS_NOT_EMPTY,
|
||||
[ViewFilterOperandDeprecated.IsRelative]: ViewFilterOperand.IS_RELATIVE,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export const convertViewFilterValueToString = (value: any) => {
|
||||
return typeof value === 'string'
|
||||
? value
|
||||
: JSON.stringify(value ?? '');
|
||||
};
|
||||
return typeof value === 'string' ? value : JSON.stringify(value ?? '');
|
||||
};
|
||||
|
||||
+1
-4
@@ -8,10 +8,7 @@ export const createAnyFieldRecordFilterBaseProperties = ({
|
||||
}: {
|
||||
filterValue: string;
|
||||
fieldMetadataItem: PartialFieldMetadataItem;
|
||||
}): Pick<
|
||||
RecordFilter,
|
||||
'id' | 'value' | 'fieldMetadataId'
|
||||
> => {
|
||||
}): Pick<RecordFilter, 'id' | 'value' | 'fieldMetadataId'> => {
|
||||
return {
|
||||
id: v4(),
|
||||
value: filterValue,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RATING_VALUES } from "@/constants";
|
||||
import { type FieldRatingValue } from "@/types";
|
||||
import { RATING_VALUES } from '@/constants';
|
||||
import { type FieldRatingValue } from '@/types';
|
||||
|
||||
export const convertGreaterThanOrEqualRatingToArrayOfRatingValues = (
|
||||
greaterThanValue: number,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { type PartialFieldMetadataItem } from "@/types";
|
||||
import { type PartialFieldMetadataItem } from '@/types';
|
||||
|
||||
export const filterSelectOptionsOfFieldMetadataItem = ({
|
||||
fieldMetadataItem,
|
||||
|
||||
@@ -385,7 +385,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new CustomError(
|
||||
throw new CustomError(
|
||||
`Unsupported empty filter type ${filterType}`,
|
||||
'UNSUPPORTED_EMPTY_FILTER_TYPE',
|
||||
);
|
||||
@@ -399,7 +399,7 @@ export const getEmptyRecordGqlOperationFilter = ({
|
||||
not: emptyRecordFilter,
|
||||
};
|
||||
default:
|
||||
throw new CustomError(
|
||||
throw new CustomError(
|
||||
`Unknown operand ${operand} for ${filterType} filter`,
|
||||
'UNKNOWN_OPERAND_FOR_FILTER',
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FieldMetadataType, type FilterableAndTSVectorFieldType } from "@/types";
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type FilterableAndTSVectorFieldType,
|
||||
} from '@/types';
|
||||
|
||||
export const getFilterTypeFromFieldType = (
|
||||
fieldType: FieldMetadataType,
|
||||
@@ -46,4 +48,4 @@ export const getFilterTypeFromFieldType = (
|
||||
default:
|
||||
return 'TEXT';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,14 +11,12 @@ export const isExpectedSubFieldName = <
|
||||
subFieldName: PossibleSubFieldName,
|
||||
subFieldNameToCheck: string | null | undefined,
|
||||
): subFieldNameToCheck is PossibleSubFieldName => {
|
||||
const allowedSubFields = Object.values(COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES[
|
||||
fieldMetadataType
|
||||
]) as readonly string[];
|
||||
const allowedSubFields = Object.values(
|
||||
COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES[fieldMetadataType],
|
||||
) as readonly string[];
|
||||
|
||||
return (
|
||||
allowedSubFields.includes(subFieldName as string) &&
|
||||
subFieldName === subFieldNameToCheck
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -169,7 +169,10 @@ export type ResolvedDateViewFilterValue<O extends ViewFilterOperand> =
|
||||
? ReturnType<typeof resolveVariableDateViewFilterValue>
|
||||
: Date | null;
|
||||
|
||||
type PartialViewFilter<O extends ViewFilterOperand> = {value: string, operand: O} // TODO, was done to avoid ViewFilter export
|
||||
type PartialViewFilter<O extends ViewFilterOperand> = {
|
||||
value: string;
|
||||
operand: O;
|
||||
}; // TODO, was done to avoid ViewFilter export
|
||||
|
||||
export const resolveDateViewFilterValue = <O extends ViewFilterOperand>(
|
||||
viewFilter: PartialViewFilter<O>,
|
||||
|
||||
@@ -19,6 +19,7 @@ export { computeDiffBetweenObjects } from './compute-diff-between-objects';
|
||||
export { deepMerge } from './deepMerge';
|
||||
export { CustomError } from './errors/CustomError';
|
||||
export { extractAndSanitizeObjectStringFields } from './extractAndSanitizeObjectStringFields';
|
||||
export { computeMorphRelationFieldName } from './fieldMetadata/compute-morph-relation-field-name';
|
||||
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
|
||||
export { checkIfShouldComputeEmptinessFilter } from './filter/checkIfShouldComputeEmptinessFilter';
|
||||
export { checkIfShouldSkipFiltering } from './filter/checkIfShouldSkipFiltering';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { isDefined } from "@/utils/validation";
|
||||
import { isDefined } from '@/utils/validation';
|
||||
|
||||
export const parseJson = <T>(rawJson: string | boolean | null | number | undefined): T | null => {
|
||||
export const parseJson = <T>(
|
||||
rawJson: string | boolean | null | number | undefined,
|
||||
): T | null => {
|
||||
try {
|
||||
if (!isDefined(rawJson)) {
|
||||
return null;
|
||||
@@ -11,8 +13,8 @@ export const parseJson = <T>(rawJson: string | boolean | null | number | undefin
|
||||
}
|
||||
|
||||
// This is a hack to handle the case where the value is a scalar value which is part of JSON spec but not implemented before ES2019
|
||||
return JSON.parse("[" + rawJson + "]")[0];
|
||||
return JSON.parse('[' + rawJson + ']')[0];
|
||||
} catch {
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,18 +5,33 @@ import {
|
||||
} from '@/types/RelativeDateValue';
|
||||
import { z } from 'zod';
|
||||
|
||||
const RelativeDateValueSchema = z.object({
|
||||
direction: z.enum(['NEXT', 'THIS', 'PAST'] as const) as z.ZodType<VariableDateViewFilterValueDirection>,
|
||||
unit: z.enum(['DAY', 'WEEK', 'MONTH', 'YEAR'] as const) as z.ZodType<VariableDateViewFilterValueUnit>,
|
||||
amount: z.number().positive().optional(),
|
||||
}).refine((data) => {
|
||||
if (data.direction === 'NEXT' || data.direction === 'PAST') {
|
||||
return data.amount !== undefined && data.amount > 0;
|
||||
}
|
||||
return true;
|
||||
}, {
|
||||
error: 'Amount is required for NEXT and PAST directions and must be positive'
|
||||
});
|
||||
const RelativeDateValueSchema = z
|
||||
.object({
|
||||
direction: z.enum([
|
||||
'NEXT',
|
||||
'THIS',
|
||||
'PAST',
|
||||
] as const) as z.ZodType<VariableDateViewFilterValueDirection>,
|
||||
unit: z.enum([
|
||||
'DAY',
|
||||
'WEEK',
|
||||
'MONTH',
|
||||
'YEAR',
|
||||
] as const) as z.ZodType<VariableDateViewFilterValueUnit>,
|
||||
amount: z.number().positive().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.direction === 'NEXT' || data.direction === 'PAST') {
|
||||
return data.amount !== undefined && data.amount > 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
error:
|
||||
'Amount is required for NEXT and PAST directions and must be positive',
|
||||
},
|
||||
);
|
||||
|
||||
export const safeParseRelativeDateFilterValue = (
|
||||
value: string,
|
||||
@@ -34,4 +49,4 @@ export const safeParseRelativeDateFilterValue = (
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './capitalize';
|
||||
|
||||
|
||||
@@ -16,8 +16,10 @@ export const TIPTAP_NODE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
} as const;
|
||||
|
||||
export type TipTapMarkType = typeof TIPTAP_MARK_TYPES[keyof typeof TIPTAP_MARK_TYPES];
|
||||
export type TipTapNodeType = typeof TIPTAP_NODE_TYPES[keyof typeof TIPTAP_NODE_TYPES];
|
||||
export type TipTapMarkType =
|
||||
(typeof TIPTAP_MARK_TYPES)[keyof typeof TIPTAP_MARK_TYPES];
|
||||
export type TipTapNodeType =
|
||||
(typeof TIPTAP_NODE_TYPES)[keyof typeof TIPTAP_NODE_TYPES];
|
||||
|
||||
// Order for mark rendering (inner to outer)
|
||||
export const TIPTAP_MARKS_RENDER_ORDER: readonly TipTapMarkType[] = [
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export const throwIfNotDefined = <T>(
|
||||
value: T,
|
||||
variableName: string
|
||||
variableName: string,
|
||||
): asserts value is NonNullable<T> => {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(`Value must be defined for variable ${variableName}, this should not happen`);
|
||||
throw new Error(
|
||||
`Value must be defined for variable ${variableName}, this should not happen`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => {
|
||||
// if the hostname is a number, it's not a valid url
|
||||
// if we let URL() parse it, it will throw cast an IP address and we lose the information
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
code: 'custom',
|
||||
message: 'domain is not a valid url',
|
||||
});
|
||||
|
||||
@@ -28,14 +28,14 @@ export const absoluteUrlSchema = z.string().transform((value, ctx) => {
|
||||
return absoluteUrl;
|
||||
}
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
code: 'custom',
|
||||
message: 'domain is not a valid url',
|
||||
});
|
||||
|
||||
return z.NEVER;
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
code: 'custom',
|
||||
message: 'domain is not a valid url',
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -16,7 +16,9 @@ describe('assertIsDefinedOrThrow', () => {
|
||||
});
|
||||
|
||||
it('throws the default error when value is undefined', () => {
|
||||
expect(() => assertIsDefinedOrThrow(undefined)).toThrow('Value not defined');
|
||||
expect(() => assertIsDefinedOrThrow(undefined)).toThrow(
|
||||
'Value not defined',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws the default error when value is null', () => {
|
||||
@@ -38,4 +40,4 @@ describe('assertIsDefinedOrThrow', () => {
|
||||
// After the assertion, TypeScript should treat `maybe` as `string`.
|
||||
expectString(maybe);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,7 @@ import { isDefined } from '@/utils';
|
||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||
export function assertIsDefinedOrThrow<T>(
|
||||
value: T | undefined | null,
|
||||
exceptionToThrow: Error = new Error(
|
||||
'Value not defined',
|
||||
),
|
||||
exceptionToThrow: Error = new Error('Value not defined'),
|
||||
): asserts value is T {
|
||||
if (!isDefined(value)) throw exceptionToThrow;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user