diff --git a/packages/stream_chat/lib/src/models/filter.dart b/packages/stream_chat/lib/src/models/filter.dart new file mode 100644 index 00000000..2a2f3410 --- /dev/null +++ b/packages/stream_chat/lib/src/models/filter.dart @@ -0,0 +1,197 @@ +// ignore_for_file: non_constant_identifier_names, constant_identifier_names + +const _groupOperators = [ + FilterOperator.and, + FilterOperator.or, + FilterOperator.nor, +]; + +/// Possible operators to use in filters. +enum FilterOperator { + /// Matches values that are equal to a specified value. + equal, + + /// Matches all values that are not equal to a specified value. + notEqual, + + /// Matches values that are greater than a specified value. + greater, + + /// Matches values that are greater than a specified value. + greaterOrEqual, + + /// Matches values that are less than a specified value. + less, + + /// Matches values that are less than or equal to a specified value. + lessOrEqual, + + /// Matches any of the values specified in an array. + in_, + + /// Matches none of the values specified in an array. + notIn, + + /// Matches values by performing text search with the specified value. + query, + + /// Matches values with the specified prefix. + autoComplete, + + /// Matches values that exist/don't exist based on the specified boolean value. + exists, + + /// Matches all the values specified in an array. + and, + + /// Matches at least one of the values specified in an array. + or, + + /// Matches none of the values specified in an array. + nor, +} + +/// Helper extension for [FilterOperator] +extension FilterOperatorX on FilterOperator { + /// Converts [FilterOperator] into rew values + String get rawValue => { + FilterOperator.equal: '\$eq', + FilterOperator.notEqual: '\$ne', + FilterOperator.greater: '\$gt', + FilterOperator.greaterOrEqual: '\$gte', + FilterOperator.less: '\$lt', + FilterOperator.lessOrEqual: '\$lte', + FilterOperator.in_: '\$in', + FilterOperator.notIn: '\$nin', + FilterOperator.query: '\$q', + FilterOperator.autoComplete: '\$autocomplete', + FilterOperator.exists: '\$exists', + FilterOperator.and: '\$and', + FilterOperator.or: '\$or', + FilterOperator.nor: '\$nor', + }[this]!; +} + +/// Stream supports a limited set of filters for querying channels, +/// users and members. The example below shows how to filter for channels +/// of type messaging where the current user is a member +/// +/// ```dart +/// val filter = Filter.and( +/// Filter.equal('type', 'messaging'), +/// Filter.in_('members', [user.id]) +/// ) +/// ``` +/// See Query Channels Documentation +class Filter { + const Filter.__({ + required this.operator, + required this.value, + this.key, + }); + + Filter._({ + required FilterOperator operator, + required this.value, + this.key, + }) : operator = operator.rawValue; + + /// Combines the provided filters and matches the values + /// matched by all filters. + factory Filter.and(List filters) => + Filter._(operator: FilterOperator.and, value: filters); + + /// Combines the provided filters and matches the values + /// matched by at least one of the filters. + factory Filter.or(List filters) => + Filter._(operator: FilterOperator.or, value: filters); + + /// Combines the provided filters and matches the values + /// not matched by all the filters. + factory Filter.nor(List filters) => + Filter._(operator: FilterOperator.nor, value: filters); + + /// Matches values that are equal to a specified value. + factory Filter.equal(String key, Object value) => + Filter._(operator: FilterOperator.equal, key: key, value: value); + + /// Matches all values that are not equal to a specified value. + factory Filter.notEqual(String key, Object value) => + Filter._(operator: FilterOperator.notEqual, key: key, value: value); + + /// Matches values that are greater than a specified value. + factory Filter.greater(String key, Object value) => + Filter._(operator: FilterOperator.greater, key: key, value: value); + + /// Matches values that are greater than a specified value. + factory Filter.greaterOrEqual(String key, Object value) => + Filter._(operator: FilterOperator.greaterOrEqual, key: key, value: value); + + /// Matches values that are less than a specified value. + factory Filter.less(String key, Object value) => + Filter._(operator: FilterOperator.less, key: key, value: value); + + /// Matches values that are less than or equal to a specified value. + factory Filter.lessOrEqual(String key, Object value) => + Filter._(operator: FilterOperator.lessOrEqual, key: key, value: value); + + /// Matches any of the values specified in an array. + factory Filter.in_(String key, List values) => + Filter._(operator: FilterOperator.in_, key: key, value: values); + + /// Matches none of the values specified in an array. + factory Filter.notIn(String key, List values) => + Filter._(operator: FilterOperator.notIn, key: key, value: values); + + /// Matches values by performing text search with the specified value. + factory Filter.query(String key, String text) => + Filter._(operator: FilterOperator.query, key: key, value: text); + + /// Matches values with the specified prefix. + factory Filter.autoComplete(String key, String text) => + Filter._(operator: FilterOperator.autoComplete, key: key, value: text); + + /// Matches values that exist/don't exist based on the specified boolean value. + factory Filter.exists(String key, {bool exists = true}) => + Filter._(operator: FilterOperator.exists, key: key, value: exists); + + /// Creates a custom [Filter] if there isn't one already available. + factory Filter.custom({ + required String operator, + required Object value, + String? key, + }) = Filter.__; + + /// Operator to use in filter. + final String operator; + + /// + final String? key; + + /// + final Object /*List|List|String*/ value; + + /// + Map toJson() { + final json = {}; + final groupOperators = _groupOperators.map((it) => it.rawValue); + + assert( + groupOperators.contains(operator) || key != null, + 'Filter must contain the `key` when the operator is not a ' + 'group operator.', + ); + + if (groupOperators.contains(operator)) { + // Filters with group operators are encoded in the following form: + // { $: [ , ] } + json[operator] = value; + } else { + // Normal filters are encoded in the following form: + // { key: { $: } } + json[key!] = {operator: value}; + } + + return json; + } +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index ef146180..2ac56b10 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -28,6 +28,7 @@ export './src/models/channel_state.dart'; export './src/models/command.dart'; export './src/models/device.dart'; export './src/models/event.dart'; +export './src/models/filter.dart' show Filter; export './src/models/member.dart'; export './src/models/message.dart'; export './src/models/mute.dart'; diff --git a/packages/stream_chat/test/src/models/filter_test.dart b/packages/stream_chat/test/src/models/filter_test.dart new file mode 100644 index 00000000..3a61a9c1 --- /dev/null +++ b/packages/stream_chat/test/src/models/filter_test.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; + +import 'package:test/test.dart'; +import 'package:stream_chat/src/models/filter.dart'; + +void main() { + group('operators', () { + test('equal', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.equal(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.equal.rawValue); + }); + + test('notEqual', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.notEqual(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.notEqual.rawValue); + }); + + test('greater', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.greater(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.greater.rawValue); + }); + + test('greaterOrEqual', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.greaterOrEqual(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.greaterOrEqual.rawValue); + }); + + test('less', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.less(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.less.rawValue); + }); + + test('lessOrEqual', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.lessOrEqual(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.lessOrEqual.rawValue); + }); + + test('in', () { + const key = 'testKey'; + const values = ['testValue']; + final filter = Filter.in_(key, values); + expect(filter.key, key); + expect(filter.value, values); + expect(filter.operator, FilterOperator.in_.rawValue); + }); + + test('in', () { + const key = 'testKey'; + const values = ['testValue']; + final filter = Filter.in_(key, values); + expect(filter.key, key); + expect(filter.value, values); + expect(filter.operator, FilterOperator.in_.rawValue); + }); + + test('notIn', () { + const key = 'testKey'; + const values = ['testValue']; + final filter = Filter.notIn(key, values); + expect(filter.key, key); + expect(filter.value, values); + expect(filter.operator, FilterOperator.notIn.rawValue); + }); + + test('query', () { + const key = 'testKey'; + const value = 'testQuery'; + final filter = Filter.query(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.query.rawValue); + }); + + test('autoComplete', () { + const key = 'testKey'; + const value = 'testQuery'; + final filter = Filter.autoComplete(key, value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, FilterOperator.autoComplete.rawValue); + }); + + test('exists', () { + const key = 'testKey'; + final filter = Filter.exists(key); + expect(filter.key, key); + expect(filter.value, isTrue); + expect(filter.operator, FilterOperator.exists.rawValue); + }); + + test('notExists', () { + const key = 'testKey'; + final filter = Filter.exists(key, exists: false); + expect(filter.key, key); + expect(filter.value, isFalse); + expect(filter.operator, FilterOperator.exists.rawValue); + }); + + test('custom', () { + const key = 'testKey'; + const value = 'testValue'; + const operator = '\$customOperator'; + final filter = Filter.custom(operator: operator, key: key, value: value); + expect(filter.key, key); + expect(filter.value, value); + expect(filter.operator, operator); + }); + + group('groupedOperator', () { + final filter1 = Filter.equal('testKey', 'testValue'); + final filter2 = Filter.in_('testKey', ['testValue']); + final filters = [filter1, filter2]; + + test('and', () { + final filter = Filter.and(filters); + expect(filter.key, isNull); + expect(filter.value, filters); + expect(filter.operator, FilterOperator.and.rawValue); + }); + + test('or', () { + final filter = Filter.or(filters); + expect(filter.key, isNull); + expect(filter.value, filters); + expect(filter.operator, FilterOperator.or.rawValue); + }); + + test('nor', () { + final filter = Filter.nor(filters); + expect(filter.key, isNull); + expect(filter.value, filters); + expect(filter.operator, FilterOperator.nor.rawValue); + }); + }); + }); + + group('encoding', () { + group('nonGroupedFilter', () { + test('simpleValue', () { + const key = 'testKey'; + const value = 'testValue'; + final filter = Filter.equal(key, value); + final encoded = json.encode(filter); + expect( + encoded, + '{"$key":{"${FilterOperator.equal.rawValue}":${json.encode(value)}}}', + ); + }); + test('listValue', () { + const key = 'testKey'; + const values = ['testValue']; + final filter = Filter.in_(key, values); + final encoded = json.encode(filter); + expect( + encoded, + '{"$key":{"${FilterOperator.in_.rawValue}":${json.encode(values)}}}', + ); + }); + }); + + test('groupedFilter', () { + final filter1 = Filter.equal('testKey', 'testValue'); + final filter2 = Filter.in_('testKey', ['testValue']); + final filters = [filter1, filter2]; + + final filter = Filter.and(filters); + final encoded = json.encode(filter); + expect( + encoded, + '{"${FilterOperator.and.rawValue}":${json.encode(filters)}}', + ); + }); + }); +}