From 72a2ec40a7d2380bd85d4c714013773100c037da Mon Sep 17 00:00:00 2001 From: Anthony K GROSS Date: Fri, 18 Aug 2023 11:36:20 +0200 Subject: [PATCH 1/3] orWhere --- src/index.ts | 107 ++++++++++++---------- tests/index.test.ts | 218 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 48 deletions(-) diff --git a/src/index.ts b/src/index.ts index 582f5fe..7fd990c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export {}; declare global { interface Array { where(filters: Filter[]): T[]; + orWhere(filters: Filter[]): T[]; } } @@ -34,57 +35,67 @@ interface Filter { value?: any; } +const hhh = function (item: T, filter: Filter): boolean { + const fieldValue = item[filter.field as keyof T]; + + switch (filter.operator) { + case '<': + return typeof fieldValue === 'number' && fieldValue < filter.value; + case '<=': + return typeof fieldValue === 'number' && fieldValue <= filter.value; + case '==': + return fieldValue === filter.value; + case '>': + return typeof fieldValue === 'number' && fieldValue > filter.value; + case '>=': + return typeof fieldValue === 'number' && fieldValue >= filter.value; + case '!=': + return fieldValue !== filter.value; + case 'array-contains': + return Array.isArray(fieldValue) && fieldValue.includes(filter.value); + case 'array-contains-any': + return Array.isArray(fieldValue) && Array.isArray(filter.value) && fieldValue.some((value: any) => filter.value.includes(value)); + case 'in': + return Array.isArray(filter.value) && filter.value.includes(fieldValue); + case 'not-in': + return Array.isArray(filter.value) && !filter.value.includes(fieldValue); + case 'startswith': + return typeof fieldValue === 'string' && fieldValue.startsWith(filter.value); + case 'endswith': + return typeof fieldValue === 'string' && fieldValue.endsWith(filter.value); + case 'contains': + return typeof fieldValue === 'string' && fieldValue.includes(filter.value); + case 'regex': + return typeof fieldValue === 'string' && new RegExp(filter.value).test(fieldValue); + case 'null': + return fieldValue === null; + case 'not-null': + return fieldValue !== null; + case 'true': + return fieldValue === true; + case 'false': + return fieldValue === false; + case 'exists': + return fieldValue !== undefined; + case 'not-exists': + return fieldValue === undefined; + default: + return false; + } +} + Array.prototype.where = function (filters: Filter[]): T[] { const data = this as T[]; return data.filter((item: T) => { - return filters.every((filter: Filter) => { - const fieldValue = item[filter.field as keyof T]; - - switch (filter.operator) { - case '<': - return typeof fieldValue === 'number' && fieldValue < filter.value; - case '<=': - return typeof fieldValue === 'number' && fieldValue <= filter.value; - case '==': - return fieldValue === filter.value; - case '>': - return typeof fieldValue === 'number' && fieldValue > filter.value; - case '>=': - return typeof fieldValue === 'number' && fieldValue >= filter.value; - case '!=': - return fieldValue !== filter.value; - case 'array-contains': - return Array.isArray(fieldValue) && fieldValue.includes(filter.value); - case 'array-contains-any': - return Array.isArray(fieldValue) && Array.isArray(filter.value) && fieldValue.some((value: any) => filter.value.includes(value)); - case 'in': - return Array.isArray(filter.value) && filter.value.includes(fieldValue); - case 'not-in': - return Array.isArray(filter.value) && !filter.value.includes(fieldValue); - case 'startswith': - return typeof fieldValue === 'string' && fieldValue.startsWith(filter.value); - case 'endswith': - return typeof fieldValue === 'string' && fieldValue.endsWith(filter.value); - case 'contains': - return typeof fieldValue === 'string' && fieldValue.includes(filter.value); - case 'regex': - return typeof fieldValue === 'string' && new RegExp(filter.value).test(fieldValue); - case 'null': - return fieldValue === null; - case 'not-null': - return fieldValue !== null; - case 'true': - return fieldValue === true; - case 'false': - return fieldValue === false; - case 'exists': - return fieldValue !== undefined; - case 'not-exists': - return fieldValue === undefined; - default: - return false; - } - }); + return filters.every((filter: Filter) => hhh(item, filter)); }); }; + +Array.prototype.orWhere = function (filters: Filter[]): T[] { + const data = this as T[]; + + return data.filter((item: T) => { + return filters.some((filter: Filter) => hhh(item, filter)); + }); +}; \ No newline at end of file diff --git a/tests/index.test.ts b/tests/index.test.ts index 505683c..6f7b093 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -217,3 +217,221 @@ describe('Array.where filters with safeguards', () => { expect(result.length).toBe(0); }); }); + +describe('Array.orWhere filters', () => { + test('<', () => { + const result = data.orWhere([ + {field: 'id', operator: '<', value: 2}, + {field: 'id', operator: '<', value: 3}, + ]); + expect(result.length).toBe(2); + }); + + test('<=', () => { + const result = data.orWhere([ + {field: 'id', operator: '<=', value: 1}, + {field: 'id', operator: '<=', value: 2}, + ]); + expect(result.length).toBe(2); + }); + + test('==', () => { + const result = data.orWhere([ + {field: 'name', operator: '==', value: 'Example 1'}, + {field: 'name', operator: '==', value: 'Example 2'}, + ]); + expect(result.length).toBe(2); + }); + test('>', () => { + const result = data.orWhere([ + { field: 'id', operator: '>', value: 1 }, + { field: 'id', operator: '>', value: 2 }, + ]); + expect(result.length).toBe(3); + }); + + test('>=', () => { + const result = data.orWhere([ + { field: 'id', operator: '>=', value: 2 }, + { field: 'id', operator: '>=', value: 3 }, + ]); + expect(result.length).toBe(3); + }); + + test('!=', () => { + const result = data.orWhere([ + { field: 'name', operator: '!=', value: 'Example 1' }, + { field: 'name', operator: '!=', value: 'Example 2' }, + ]); + expect(result.length).toBe(4); + }); + + test('array-contains', () => { + const result = data.orWhere([ + { field: 'tags', operator: 'array-contains', value: 'tag1' }, + { field: 'tags', operator: 'array-contains', value: 'tag2' }, + ]); + expect(result.length).toBe(3); + }); + + test('array-contains-any', () => { + const result = data.orWhere([ + { field: 'tags', operator: 'array-contains-any', value: ['tag1', 'tag3'] }, + { field: 'tags', operator: 'array-contains-any', value: ['tag1', 'tag2'] }, + ]); + expect(result.length).toBe(3); + }); + + test('in', () => { + const result = data.orWhere([ + { field: 'id', operator: 'in', value: [1, 3] }, + { field: 'id', operator: 'in', value: [1, 2] }, + ]); + expect(result.length).toBe(3); + }); + + test('not-in', () => { + const result = data.orWhere([ + { field: 'id', operator: 'not-in', value: [1, 3] }, + { field: 'id', operator: 'not-in', value: [1, 2] }, + ]); + expect(result.length).toBe(3); + }); + + + test('startswith', () => { + const result = data.orWhere([ + { field: 'name', operator: 'startswith', value: 'Example 2' }, + { field: 'name', operator: 'startswith', value: 'Example 1' }, + ]); + expect(result.length).toBe(2); + }); + + test('endswith', () => { + const result = data.orWhere([ + { field: 'name', operator: 'endswith', value: '3' }, + { field: 'name', operator: 'endswith', value: '2' }, + ]); + expect(result.length).toBe(2); + }); + + test('contains', () => { + const result = data.orWhere([ + { field: 'name', operator: 'contains', value: 'ple 1' }, + { field: 'name', operator: 'contains', value: 'Ex' }, + ]); + expect(result.length).toBe(4); + }); + + test('regex', () => { + const result = data.orWhere([ + { field: 'name', operator: 'regex', value: /^Example\s\d$/ }, + { field: 'name', operator: 'regex', value: /^example\s\d$/ }, + ]); + expect(result.length).toBe(4); + }); + + test('null', () => { + const result = data.orWhere([ + { field: 'optional', operator: 'null' }, + ]); + expect(result.length).toBe(1); + }); + + test('not-null', () => { + const result = data.orWhere([ + { field: 'optional', operator: 'not-null' }, + ]); + expect(result.length).toBe(3); + }); + + test('true', () => { + const result = data.orWhere([ + { field: 'active', operator: 'true' }, + { field: 'active', operator: 'false' }, + ]); + expect(result.length).toBe(4); + }); + + test('false', () => { + const result = data.orWhere([ + { field: 'active', operator: 'false' }, + { field: 'active', operator: 'true' }, + ]); + expect(result.length).toBe(4); + }); + + test('exists', () => { + const result = data.orWhere([ + { field: 'optional', operator: 'exists' }, + ]); + expect(result.length).toBe(2); + }); + + test('not-exists', () => { + const result = data.orWhere([ + { field: 'optional', operator: 'not-exists' }, + ]); + expect(result.length).toBe(2); + }); +}); + + +describe('Array.orWhere filters with safeguards', () => { + + test('invalid number comparison', () => { + const result = data.orWhere([ + { field: 'name', operator: '<', value: 1 }, + ]); + expect(result.length).toBe(0); + }); + + test('array-contains-any with non-array filter value', () => { + const result = data.orWhere([ + { field: 'tags', operator: 'array-contains-any', value: 'tag1' }, + ]); + expect(result.length).toBe(0); + }); + + test('in with non-array filter value', () => { + const result = data.orWhere([ + { field: 'id', operator: 'in', value: 1 }, + ]); + expect(result.length).toBe(0); + }); + + test('not-in with non-array filter value', () => { + const result = data.orWhere([ + { field: 'id', operator: 'not-in', value: 1 }, + ]); + expect(result.length).toBe(0); + }); + + test('optional field with undefined value', () => { + const result = data.orWhere([ + { field: 'optional', operator: '==', value: undefined }, + ]); + expect(result.length).toBe(2); + }); + + test('Invalid operator', () => { + const result = data.orWhere([ + { field: 'id', operator: 'invalid-operator' as FilterOperator, value: 1 }, + ]); + expect(result.length).toBe(0); + }); + + test('Invalid field', () => { + const result = data.orWhere([ + { field: 'invalid-field' as keyof Example, operator: '==', value: 'Example 1' }, + ]); + expect(result.length).toBe(0); + }); + + test('Invalid value type for string filters', () => { + const result = data.orWhere([ + { field: 'name', operator: 'startswith', value: { notAString: true } }, + ]); + expect(result.length).toBe(0); + }); +}); \ No newline at end of file From e3bd985ddb128d81d66b9228c546d7e148d8e14b Mon Sep 17 00:00:00 2001 From: Anthony K GROSS Date: Fri, 18 Aug 2023 11:45:33 +0200 Subject: [PATCH 2/3] Method name and READ.md --- README.md | 22 ++++++++++++++++++++-- src/index.ts | 6 +++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 01dae8c..e210c00 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,27 @@ Apply filters using the .where(...) method: ```typescript const result = data.where([ -{ field: 'id', operator: '>', value: 1 }, -{ field: 'tags', operator: 'array-contains', value: 'tag3' }, + { field: 'id', operator: '>', value: 1 }, + { field: 'tags', operator: 'array-contains', value: 'tag3' }, ]); +// Expected output: [ +// { id: 1, name: 'Example 1', tags: ['tag1', 'tag2'] }, +// { id: 2, name: 'Example 2', tags: ['tag2', 'tag3'] }, +// { id: 3, name: 'Example 3', tags: ['tag1', 'tag3'] }, +// ] +``` + +Apply filters using the .orWhere(...) method: + +```typescript +const result = data.orWhere([ + { field: 'id', operator: '==', value: 1 }, + { field: 'id', operator: '==', value: 2 }, +]); +// Expected output: [ +// { id: 1, name: 'Example 1', tags: ['tag1', 'tag2'] }, +// { id: 2, name: 'Example 2', tags: ['tag2', 'tag3'] }, +// ] ``` ## Supported Filters diff --git a/src/index.ts b/src/index.ts index 7fd990c..fbec041 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,7 +35,7 @@ interface Filter { value?: any; } -const hhh = function (item: T, filter: Filter): boolean { +const isFiltered = function (item: T, filter: Filter): boolean { const fieldValue = item[filter.field as keyof T]; switch (filter.operator) { @@ -88,7 +88,7 @@ Array.prototype.where = function (filters: Filter[]): T[] { const data = this as T[]; return data.filter((item: T) => { - return filters.every((filter: Filter) => hhh(item, filter)); + return filters.every((filter: Filter) => isFiltered(item, filter)); }); }; @@ -96,6 +96,6 @@ Array.prototype.orWhere = function (filters: Filter[]): T[] { const data = this as T[]; return data.filter((item: T) => { - return filters.some((filter: Filter) => hhh(item, filter)); + return filters.some((filter: Filter) => isFiltered(item, filter)); }); }; \ No newline at end of file From c9fcb4bd279ae7e16e8f01e0c3514dac79f990ef Mon Sep 17 00:00:00 2001 From: Anthony K GROSS Date: Fri, 18 Aug 2023 11:47:18 +0200 Subject: [PATCH 3/3] Fix README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e210c00..7231bd3 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,10 @@ Apply filters using the .where(...) method: ```typescript const result = data.where([ { field: 'id', operator: '>', value: 1 }, - { field: 'tags', operator: 'array-contains', value: 'tag3' }, + { field: 'tags', operator: 'array-contains', value: 'tag2' }, ]); // Expected output: [ -// { id: 1, name: 'Example 1', tags: ['tag1', 'tag2'] }, // { id: 2, name: 'Example 2', tags: ['tag2', 'tag3'] }, -// { id: 3, name: 'Example 3', tags: ['tag1', 'tag3'] }, // ] ```