Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,25 @@ 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: 'tag2' },
]);
// Expected output: [
// { id: 2, name: 'Example 2', tags: ['tag2', '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
Expand Down
107 changes: 59 additions & 48 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {};
declare global {
interface Array<T> {
where(filters: Filter<T>[]): T[];
orWhere(filters: Filter<T>[]): T[];
}
}

Expand Down Expand Up @@ -34,57 +35,67 @@ interface Filter<T> {
value?: any;
}

const isFiltered = function<T> (item: T, filter: Filter<T>): 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 <T>(filters: Filter<T>[]): T[] {
const data = this as T[];

return data.filter((item: T) => {
return filters.every((filter: Filter<T>) => {
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<T>) => isFiltered(item, filter));
});
};

Array.prototype.orWhere = function <T>(filters: Filter<T>[]): T[] {
const data = this as T[];

return data.filter((item: T) => {
return filters.some((filter: Filter<T>) => isFiltered(item, filter));
});
};
218 changes: 218 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});