Skip to content

Commit 9fdbc8e

Browse files
committed
fix(postgrest): review fixes for typed filters and asStream
1 parent 03af7a3 commit 9fdbc8e

4 files changed

Lines changed: 84 additions & 3 deletions

File tree

packages/postgrest/lib/src/postgrest_table.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -711,5 +711,14 @@ final class NegatedFilter extends ColumnFilter {
711711
@override
712712
PostgrestFilterBuilder<dynamic> _apply(
713713
PostgrestFilterBuilder<dynamic> builder,
714-
) => builder.not(column, inner.operator, inner.value);
714+
) {
715+
// The untyped `not` stringifies map values with `Map.toString`, unlike
716+
// the json-encoding positive paths such as `contains`, so encode here.
717+
final innerValue = inner.value;
718+
return builder.not(
719+
column,
720+
inner.operator,
721+
innerValue is Map ? json.encode(innerValue) : innerValue,
722+
);
723+
}
715724
}

packages/postgrest/lib/src/postgrest_typed_builder.dart

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:convert';
23

34
import 'package:postgrest/postgrest.dart';
45

@@ -37,7 +38,24 @@ class PostgrestTypedBuilder<T> implements Future<T> {
3738
}
3839

3940
@override
40-
Stream<T> asStream() => _execute().asStream();
41+
Stream<T> asStream() {
42+
// Mirrors [PostgrestBuilder.asStream], which returns a broadcast stream.
43+
final controller = StreamController<T>.broadcast();
44+
45+
unawaited(
46+
then((value) {
47+
controller.add(value);
48+
})
49+
.catchError((Object error, StackTrace stack) {
50+
controller.addError(error, stack);
51+
})
52+
.whenComplete(() {
53+
unawaited(controller.close());
54+
}),
55+
);
56+
57+
return controller.stream;
58+
}
4159

4260
@override
4361
Future<T> catchError(Function onError, {bool Function(Object error)? test}) =>

packages/postgrest/lib/src/postgrest_typed_filter_builder.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ class PostgrestTypedFilterBuilder<Row, T>
4242
/// .whereAny([Books.id.eq(1), Books.title.eq('foo')]);
4343
/// ```
4444
PostgrestTypedFilterBuilder<Row, T> whereAny(List<ColumnFilter> filters) {
45+
if (filters.isEmpty) {
46+
throw ArgumentError.value(
47+
filters,
48+
'filters',
49+
'whereAny needs at least one filter',
50+
);
51+
}
4552
final fragments = [for (final filter in filters) _orFragment(filter)];
4653
return PostgrestTypedFilterBuilder._(
4754
_filterBuilder.or(fragments.join(',')),
@@ -69,7 +76,9 @@ class PostgrestTypedFilterBuilder<Row, T>
6976
if (value == null || value is num || value is bool) {
7077
return '$value';
7178
}
72-
final escaped = '$value'.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
79+
// Maps are encoded as json, matching the positive containment paths.
80+
final rendered = value is Map ? json.encode(value) : '$value';
81+
final escaped = rendered.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
7382
return '"$escaped"';
7483
}
7584
}

packages/postgrest/test/typed_query_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Books {
1818
static const title = TableColumn<String>('title');
1919
static const tags = TableColumn<List<String>>('tags');
2020
static const ageRange = TableColumn<String>('age_range');
21+
static const metadata = TableColumn<Map<String, dynamic>>('metadata');
2122
}
2223

2324
class MockHttpClient extends BaseClient {
@@ -227,6 +228,50 @@ void main() {
227228
test('negating a filter twice throws', () {
228229
expect(() => Books.id.eq(1).not().not(), throwsStateError);
229230
});
231+
232+
test('negated json containment encodes the value as json', () async {
233+
await client
234+
.table(Books.table)
235+
.select()
236+
.where(Books.metadata.contains({'a': 1}).not());
237+
238+
expect(requestParameters()['metadata'], 'not.cs.{"a":1}');
239+
});
240+
241+
test('whereAny encodes json containment values', () async {
242+
await client.table(Books.table).select().whereAny([
243+
Books.metadata.contains({'a': 1}),
244+
Books.id.eq(1),
245+
]);
246+
247+
expect(
248+
requestParameters()['or'],
249+
r'(metadata.cs."{\"a\":1}",id.eq.1)',
250+
);
251+
});
252+
253+
test('whereAny without filters throws', () {
254+
expect(
255+
() => client.table(Books.table).select().whereAny([]),
256+
throwsArgumentError,
257+
);
258+
});
259+
});
260+
261+
group('asStream', () {
262+
test('returns a broadcast stream that supports multiple listeners', () {
263+
httpClient.responseBody = bookRows;
264+
265+
final stream = client.table(Books.table).select().asStream();
266+
267+
expect(stream.isBroadcast, isTrue);
268+
stream.listen(
269+
expectAsync1((books) {
270+
expect(books, hasLength(2));
271+
}),
272+
);
273+
stream.listen(expectAsync1((books) {}));
274+
});
230275
});
231276

232277
group('transforms', () {

0 commit comments

Comments
 (0)