Skip to content

Commit 08176d3

Browse files
authored
feat(search): update query params build (algolia#8)
1 parent 6452a90 commit 08176d3

13 files changed

+824
-110
lines changed
File renamed without changes.

helper_dart/lib/src/filter.dart

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:meta/meta.dart';
2+
13
/// Represents a search filter
2-
class Filter {
4+
abstract class Filter {
35
const Filter._(this.attribute, this.isNegated);
46

57
final String attribute;
@@ -8,10 +10,10 @@ class Filter {
810
/// Creates [FilterFacet] instance.
911
static FilterFacet facet(
1012
String attribute,
11-
dynamic value, [
13+
dynamic value, {
1214
bool isNegated = false,
1315
int? score,
14-
]) =>
16+
}) =>
1517
FilterFacet._(attribute, value, isNegated, score);
1618

1719
/// Creates [FilterTag] instance.
@@ -22,19 +24,23 @@ class Filter {
2224
static FilterNumeric comparison(
2325
String attribute,
2426
NumericOperator operator,
25-
num number, [
27+
num number, {
2628
bool isNegated = false,
27-
]) =>
29+
}) =>
2830
FilterNumeric.comparison(attribute, operator, number, isNegated);
2931

3032
/// Creates [FilterNumeric] instance as numeric range.
3133
static FilterNumeric range(
32-
String attribute,
33-
num lowerBound,
34-
num upperBound, [
34+
String attribute, {
35+
required num lowerBound,
36+
required num upperBound,
3537
bool isNegated = false,
36-
]) =>
38+
}) =>
3739
FilterNumeric.range(attribute, lowerBound, upperBound, isNegated);
40+
41+
/// Negates a [FilterFacet].
42+
@factory
43+
Filter not();
3844
}
3945

4046
/// A [FilterFacet] matches exactly an [attribute] with a [value].
@@ -84,12 +90,15 @@ class FilterFacet implements Filter {
8490
bool? isNegated,
8591
int? score,
8692
}) =>
87-
Filter.facet(
93+
FilterFacet._(
8894
attribute ?? this.attribute,
8995
value ?? this.value,
9096
isNegated ?? this.isNegated,
9197
score ?? this.score,
9298
);
99+
100+
@override
101+
FilterFacet not() => copyWith(isNegated: !isNegated);
93102
}
94103

95104
/// A [FilterTag] filters on a specific [value].
@@ -127,10 +136,13 @@ class FilterTag implements Filter {
127136
String? value,
128137
bool? isNegated,
129138
}) =>
130-
Filter.tag(
139+
FilterTag._(
131140
value ?? this.value,
132141
isNegated ?? this.isNegated,
133142
);
143+
144+
@override
145+
FilterTag not() => copyWith(isNegated: !isNegated);
134146
}
135147

136148
/// A [FilterNumeric] filters on a numeric [value].
@@ -176,6 +188,9 @@ class FilterNumeric implements Filter {
176188
value ?? this.value,
177189
isNegated ?? this.isNegated,
178190
);
191+
192+
@override
193+
FilterNumeric not() => copyWith(isNegated: !isNegated);
179194
}
180195

181196
/// Represents a filter numeric value.

helper_dart/lib/src/filter_group.dart

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import 'package:collection/collection.dart';
2+
3+
import 'extensions.dart';
14
import 'filter.dart';
2-
import 'utils.dart';
35

46
/// Identifier of a filter group.
57
/// The group name is for access purpose only, won't be used for the actual
@@ -39,45 +41,45 @@ class FilterGroupID {
3941
enum FilterOperator { and, or }
4042

4143
/// Represents a filter group
42-
abstract class FilterGroup<T> {
43-
const FilterGroup._(this.groupID, this.filters);
44+
abstract class FilterGroup<T> extends DelegatingSet<T> {
45+
const FilterGroup._(this.groupID, this._filters) : super(_filters);
4446

4547
/// Create [FilterGroup] as [FacetFilterGroup].
46-
static FacetFilterGroup facet([
48+
static FacetFilterGroup facet({
4749
String name = '',
4850
Set<FilterFacet> filters = const {},
4951
FilterOperator operator = FilterOperator.and,
50-
]) =>
52+
}) =>
5153
FacetFilterGroup(FilterGroupID(name, operator), filters);
5254

5355
/// Create [FilterGroup] as [TagFilterGroup].
54-
static TagFilterGroup tag([
56+
static TagFilterGroup tag({
5557
String name = '',
5658
Set<FilterTag> filters = const {},
5759
FilterOperator operator = FilterOperator.and,
58-
]) =>
60+
}) =>
5961
TagFilterGroup(FilterGroupID(name, operator), filters);
6062

6163
/// Create [FilterGroup] as [NumericFilterGroup].
62-
static NumericFilterGroup numeric([
64+
static NumericFilterGroup numeric({
6365
String name = '',
6466
Set<FilterNumeric> filters = const {},
6567
FilterOperator operator = FilterOperator.and,
66-
]) =>
68+
}) =>
6769
NumericFilterGroup(FilterGroupID(name, operator), filters);
6870

6971
/// Create [FilterGroup] as [HierarchicalFilterGroup].
70-
static HierarchicalFilterGroup hierarchical([
72+
static HierarchicalFilterGroup hierarchical({
7173
String name = '',
7274
Set<HierarchicalFilter> filters = const {},
73-
]) =>
75+
}) =>
7476
HierarchicalFilterGroup(name, filters);
7577

7678
/// Filter group ID (name and operator)
7779
final FilterGroupID groupID;
7880

7981
/// Set of filters.
80-
final Set<T> filters;
82+
final Set<T> _filters;
8183

8284
/// Create a copy with given parameters.
8385
FilterGroup<T> copyWith({FilterGroupID? groupID, Set<T>? filters});
@@ -88,10 +90,10 @@ abstract class FilterGroup<T> {
8890
other is FilterGroup &&
8991
runtimeType == other.runtimeType &&
9092
groupID == other.groupID &&
91-
filters.equals(other.filters);
93+
_filters.equals(other._filters);
9294

9395
@override
94-
int get hashCode => groupID.hashCode ^ filters.hashing();
96+
int get hashCode => groupID.hashCode ^ _filters.hashing();
9597
}
9698

9799
/// Facets filter group
@@ -106,11 +108,12 @@ class FacetFilterGroup extends FilterGroup<FilterFacet> {
106108
}) =>
107109
FacetFilterGroup(
108110
groupID ?? this.groupID,
109-
filters ?? this.filters,
111+
filters ?? _filters,
110112
);
111113

112114
@override
113-
String toString() => 'FacetFilterGroup{groupID: $groupID, filters: $filters}';
115+
String toString() =>
116+
'FacetFilterGroup{groupID: $groupID, filters: $_filters}';
114117
}
115118

116119
/// Tags filter group
@@ -125,11 +128,11 @@ class TagFilterGroup extends FilterGroup<FilterTag> {
125128
}) =>
126129
TagFilterGroup(
127130
groupID ?? this.groupID,
128-
filters ?? this.filters,
131+
filters ?? _filters,
129132
);
130133

131134
@override
132-
String toString() => 'TagFilterGroup{groupID: $groupID, filters: $filters}';
135+
String toString() => 'TagFilterGroup{groupID: $groupID, filters: $_filters}';
133136
}
134137

135138
/// Numeric facets filter group
@@ -144,20 +147,22 @@ class NumericFilterGroup extends FilterGroup<FilterNumeric> {
144147
}) =>
145148
NumericFilterGroup(
146149
groupID ?? this.groupID,
147-
filters ?? this.filters,
150+
filters ?? _filters,
148151
);
149152

150153
@override
151154
String toString() =>
152-
'NumericFilterGroup{groupID: $groupID, filters: $filters}';
155+
'NumericFilterGroup{groupID: $groupID, filters: $_filters}';
153156
}
154157

155158
/// Hierarchical filter group
156159
class HierarchicalFilterGroup extends FilterGroup<HierarchicalFilter> {
157160
HierarchicalFilterGroup(String name, Set<HierarchicalFilter> filters)
158161
: this._(FilterGroupID(name), filters);
159162

160-
const HierarchicalFilterGroup._(super.groupID, super.filters) : super._();
163+
HierarchicalFilterGroup._(super.groupID, super.filters) : super._() {
164+
assert(groupID.operator == FilterOperator.and);
165+
}
161166

162167
/// Make a copy of the hierarchical filters group.
163168
@override
@@ -167,10 +172,10 @@ class HierarchicalFilterGroup extends FilterGroup<HierarchicalFilter> {
167172
}) =>
168173
HierarchicalFilterGroup._(
169174
groupID ?? this.groupID,
170-
filters ?? this.filters,
175+
filters ?? _filters,
171176
);
172177

173178
@override
174179
String toString() =>
175-
'HierarchicalFilterGroup{groupID: $groupID, filters: $filters}';
180+
'HierarchicalFilterGroup{groupID: $groupID, filters: $_filters}';
176181
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'package:collection/collection.dart';
2+
3+
import 'filter.dart';
4+
import 'filter_group.dart';
5+
6+
/// Converts [FilterGroup] to an SQL like syntax.
7+
class FilterGroupConverter {
8+
/// Creates [FilterGroupConverter] instance.
9+
const FilterGroupConverter();
10+
11+
/// Converts [FilterGroup] to its SQL-like [String] representation.
12+
/// Returns `null` if the list is empty.
13+
String? sql(Set<FilterGroup> filterGroups) {
14+
final groups = filterGroups
15+
.whereType<FilterGroup<Filter>>()
16+
.whereNot((element) => element.isEmpty);
17+
if (groups.isEmpty) return null;
18+
return groups.map(_sqlGroup).join(' AND ');
19+
}
20+
21+
/// Same as [sql], but removes quotes for readability purposes.
22+
String? unquoted(Set<FilterGroup<Filter>> filterGroups) =>
23+
sql(filterGroups)?.replaceAll('\"', '');
24+
25+
/// Convert a filter group to an SQL-like syntax
26+
String _sqlGroup(FilterGroup<Filter> group) {
27+
final sql = group
28+
.map((filter) => const FilterConverter().sql(filter))
29+
.join(_separatorOf(group));
30+
return '($sql)';
31+
}
32+
33+
/// Get separator string (i.e. AND/OR) of a [group].
34+
String _separatorOf(FilterGroup group) {
35+
switch (group.groupID.operator) {
36+
case FilterOperator.and:
37+
return ' AND ';
38+
case FilterOperator.or:
39+
return ' OR ';
40+
}
41+
}
42+
}
43+
44+
/// Converts [Filter] to an SQL like syntax.
45+
class FilterConverter {
46+
/// Creates [FilterConverter] instance.
47+
const FilterConverter();
48+
49+
/// Converts [Filter] to its SQL-like [String] representation.
50+
String sql(Filter filter) {
51+
switch (filter.runtimeType) {
52+
case FilterFacet:
53+
return _sqlFacet(filter as FilterFacet);
54+
case FilterTag:
55+
return _sqlTag(filter as FilterTag);
56+
case FilterNumeric:
57+
return _sqlNumeric(filter as FilterNumeric);
58+
default:
59+
throw ArgumentError('Filter type ${filter.runtimeType} not supported');
60+
}
61+
}
62+
63+
/// Converts [FilterFacet] to its SQL-like [String] representation.
64+
String _sqlFacet(FilterFacet filter) {
65+
final value = _sqlValue(filter.value);
66+
final attribute = _escape(filter.attribute);
67+
final score = filter.score != null ? '<score=${filter.score}>' : '';
68+
final expression = '$attribute:$value$score';
69+
return filter.isNegated ? 'NOT $expression' : expression;
70+
}
71+
72+
/// Converts [FilterTag] to its SQL-like [String] representation.
73+
String _sqlTag(FilterTag filter) {
74+
final attribute = filter.attribute;
75+
final escapedValue = _escape(filter.value);
76+
final expression = '$attribute:$escapedValue';
77+
return filter.isNegated ? 'NOT $expression' : expression;
78+
}
79+
80+
/// Converts [FilterNumeric] to its SQL-like [String] representation.
81+
String _sqlNumeric(FilterNumeric filter) {
82+
switch (filter.value.runtimeType) {
83+
case NumericRange:
84+
return _sqlRange(
85+
filter.value as NumericRange,
86+
filter.attribute,
87+
filter.isNegated,
88+
);
89+
case NumericComparison:
90+
return _sqlComparison(
91+
filter.value as NumericComparison,
92+
filter.attribute,
93+
filter.isNegated,
94+
);
95+
default:
96+
throw ArgumentError('Filter type ${filter.runtimeType} not supported');
97+
}
98+
}
99+
100+
/// Converts [NumericRange] to its SQL-like [String] representation.
101+
String _sqlRange(NumericRange range, String attribute, bool isNegated) {
102+
final escapedAttribute = _escape(attribute);
103+
final lowerBound = range.lowerBound;
104+
final upperBound = range.upperBound;
105+
final expression = '$escapedAttribute:$lowerBound TO $upperBound';
106+
return isNegated ? 'NOT $expression' : expression;
107+
}
108+
109+
/// Converts [FilterNumeric] with [NumericComparison] value to its SQL-like
110+
/// [String] representation.
111+
String _sqlComparison(
112+
NumericComparison comparison,
113+
String attribute,
114+
bool isNegated,
115+
) {
116+
final escapedAttribute = _escape(attribute);
117+
final operator = comparison.operator.operator;
118+
final number = comparison.number;
119+
final expression = '$escapedAttribute $operator $number';
120+
return isNegated ? 'NOT $expression' : expression;
121+
}
122+
123+
/// Converts [value] to its SQL-like [String] representation.
124+
String _sqlValue(dynamic value) {
125+
switch (value.runtimeType) {
126+
case String:
127+
return _escape(value as String);
128+
case int:
129+
case double:
130+
case bool:
131+
return value.toString();
132+
default:
133+
throw ArgumentError('value type ${value.runtimeType} not supported');
134+
}
135+
}
136+
137+
/// String escape [value].
138+
String _escape(String value) => '\"$value\"';
139+
}

helper_dart/lib/src/filters.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'package:meta/meta.dart';
22

3+
import 'extensions.dart';
34
import 'filter.dart';
45
import 'filter_group.dart';
5-
import 'utils.dart';
66

77
/// Map of filter groups convenience type.
88
typedef FilterGroupMap<T> = Map<FilterGroupID, Set<T>>;

0 commit comments

Comments
 (0)