diff --git a/lib/postgrest.dart b/lib/postgrest.dart index 139e461..273b36c 100644 --- a/lib/postgrest.dart +++ b/lib/postgrest.dart @@ -15,7 +15,8 @@ class PostgrestClient { /// new PostgrestClient(REST_URL) /// new PostgrestClient(REST_URL, headers: { 'apikey': 'foo' }) /// ``` - PostgrestClient(this.url, {Map headers, this.schema}) : headers = headers ?? {}; + PostgrestClient(this.url, {Map headers, this.schema}) + : headers = headers ?? {}; /// Authenticates the request with JWT. PostgrestClient auth(String token) { @@ -32,6 +33,7 @@ class PostgrestClient { /// Perform a stored procedure call. PostgrestBuilder rpc(String fn, Map params) { final url = '${this.url}/rpc/$fn'; - return PostgrestQueryBuilder(url, headers: headers, schema: schema).rpc(params); + return PostgrestQueryBuilder(url, headers: headers, schema: schema) + .rpc(params); } } diff --git a/lib/src/builder.dart b/lib/src/builder.dart index 009ce15..1619dda 100644 --- a/lib/src/builder.dart +++ b/lib/src/builder.dart @@ -4,6 +4,7 @@ import 'dart:core'; import 'package:http/http.dart' as http; +import 'count_option.dart'; import 'postgrest_error.dart'; import 'postgrest_response.dart'; @@ -21,7 +22,22 @@ class PostgrestBuilder { /// /// For more details about switching schemas: https://postgrest.org/en/stable/api.html#switching-schemas /// Returns {Future} Resolves when the request has completed. - Future execute() async { + Future execute({ + bool head = false, + CountOption count, + }) async { + if (head) { + method = 'HEAD'; + } + + if (count != null) { + if (headers['Prefer'] == null) { + headers['Prefer'] = 'count=${count.name()}'; + } else { + headers['Prefer'] += ',count=${count.name()}'; + } + } + try { final uppercaseMethod = method.toUpperCase(); http.Response response; @@ -43,13 +59,17 @@ class PostgrestBuilder { if (uppercaseMethod == 'GET') { response = await client.get(url, headers: headers ?? {}); } else if (uppercaseMethod == 'POST') { - response = await client.post(url, headers: headers ?? {}, body: bodyStr); + response = + await client.post(url, headers: headers ?? {}, body: bodyStr); } else if (uppercaseMethod == 'PUT') { response = await client.put(url, headers: headers ?? {}, body: bodyStr); } else if (uppercaseMethod == 'PATCH') { - response = await client.patch(url, headers: headers ?? {}, body: bodyStr); + response = + await client.patch(url, headers: headers ?? {}, body: bodyStr); } else if (uppercaseMethod == 'DELETE') { response = await client.delete(url, headers: headers ?? {}); + } else if (uppercaseMethod == 'HEAD') { + response = await client.head(url, headers: headers ?? {}); } return parseJsonResponse(response); @@ -73,15 +93,26 @@ class PostgrestBuilder { ); } else { dynamic body; - try { - body = json.decode(response.body); - } on FormatException catch (_) { - body = response.body; + int count; + if (response.request.method != 'HEAD') { + try { + body = json.decode(response.body); + } on FormatException catch (_) { + body = response.body; + } + } + + final contentRange = response.headers['content-range']; + if (contentRange != null) { + count = contentRange.split('/').last == '*' + ? null + : int.parse(contentRange.split('/').last); } return PostgrestResponse( data: body, status: response.statusCode, + count: count, ); } } @@ -103,7 +134,8 @@ class PostgrestBuilder { /// * delete() - "delete" /// Once any of these are called the filters are passed down to the Request. class PostgrestQueryBuilder extends PostgrestBuilder { - PostgrestQueryBuilder(String url, {Map headers, String schema}) { + PostgrestQueryBuilder(String url, + {Map headers, String schema}) { this.url = Uri.parse(url); this.headers = headers ?? {}; this.schema = schema; @@ -141,10 +173,15 @@ class PostgrestQueryBuilder extends PostgrestBuilder { /// postgrest.from('messages').insert({ message: 'foo', username: 'supabot', channel_id: 1 }) /// postgrest.from('messages').insert({ id: 3, message: 'foo', username: 'supabot', channel_id: 2 }, { upsert: true }) /// ``` - PostgrestBuilder insert(dynamic values, {bool upsert = false, String onConflict}) { + PostgrestBuilder insert( + dynamic values, { + bool upsert = false, + String onConflict, + }) { method = 'POST'; - headers['Prefer'] = - upsert ? 'return=representation,resolution=merge-duplicates' : 'return=representation'; + headers['Prefer'] = upsert + ? 'return=representation,resolution=merge-duplicates' + : 'return=representation'; body = values; return this; } @@ -225,7 +262,8 @@ class PostgrestTransformBuilder extends PostgrestBuilder { /// postgrest.from('users').select('messages(*)').range(1, 1, { foreignTable: 'messages' }) /// ``` PostgrestTransformBuilder range(int from, int to, {String foreignTable}) { - final keyOffset = foreignTable == null ? 'offset' : '"$foreignTable".offset'; + final keyOffset = + foreignTable == null ? 'offset' : '"$foreignTable".offset'; final keyLimit = foreignTable == null ? 'limit' : '"$foreignTable".limit'; appendSearchParams(keyOffset, '$from'); diff --git a/lib/src/count_option.dart b/lib/src/count_option.dart new file mode 100644 index 0000000..b5fed8f --- /dev/null +++ b/lib/src/count_option.dart @@ -0,0 +1,12 @@ +/// Returns count as part of the response when specified. +enum CountOption { + exact, + planned, + estimated, +} + +extension CountOptionName on CountOption { + String name() { + return toString().split('.').last; + } +} diff --git a/lib/src/postgrest_response.dart b/lib/src/postgrest_response.dart index 93d6a8d..d2a8b87 100644 --- a/lib/src/postgrest_response.dart +++ b/lib/src/postgrest_response.dart @@ -12,20 +12,24 @@ class PostgrestResponse { this.status, this.statusText, this.error, + this.count, }); final dynamic data; final int status; final String statusText; final PostgrestError error; + final int count; - factory PostgrestResponse.fromJson(Map json) => PostgrestResponse( + factory PostgrestResponse.fromJson(Map json) => + PostgrestResponse( data: json['body'], status: json['status'] as int, statusText: json['statusText'] as String, error: json['error'] == null ? null : PostgrestError.fromJson(json['error'] as Map), + count: json['count'] as int, ); Map toJson() => { @@ -33,5 +37,6 @@ class PostgrestResponse { 'status': status, 'statusText': statusText, 'error': error?.toJson(), + 'count': count, }; } diff --git a/test/basic_test.dart b/test/basic_test.dart index f63af77..b82f22f 100644 --- a/test/basic_test.dart +++ b/test/basic_test.dart @@ -1,3 +1,4 @@ +import 'package:postgrest/src/count_option.dart'; import 'package:test/test.dart'; import 'package:postgrest/postgrest.dart'; @@ -15,7 +16,8 @@ void main() { }); test('stored procedure', () async { - final res = await postgrest.rpc('get_status', {'name_param': 'supabot'}).execute(); + final res = + await postgrest.rpc('get_status', {'name_param': 'supabot'}).execute(); expect(res.data, 'ONLINE'); }); @@ -26,7 +28,8 @@ void main() { test('auth', () async { postgrest = PostgrestClient(rootUrl).auth('foo'); - expect(postgrest.from('users').select().headers['Authorization'], 'Bearer foo'); + expect(postgrest.from('users').select().headers['Authorization'], + 'Bearer foo'); }); test('switch schema', () async { @@ -36,7 +39,8 @@ void main() { }); test('on_conflict insert', () async { - final res = await postgrest.from('users').insert({'username': 'dragarcia', 'status': 'OFFLINE'}, + final res = await postgrest.from('users').insert( + {'username': 'dragarcia', 'status': 'OFFLINE'}, upsert: true, onConflict: 'username').execute(); expect(res.data[0]['status'], 'OFFLINE'); }); @@ -61,18 +65,28 @@ void main() { }); test('basic update', () async { - await postgrest.from('messages').update({'channel_id': 2}).eq('message', 'foo').execute(); - - final resMsg = - await postgrest.from('messages').select().filter('message', 'eq', 'foo').execute(); + await postgrest + .from('messages') + .update({'channel_id': 2}) + .eq('message', 'foo') + .execute(); + + final resMsg = await postgrest + .from('messages') + .select() + .filter('message', 'eq', 'foo') + .execute(); resMsg.data.forEach((rec) => expect(rec['channel_id'], 2)); }); test('basic delete', () async { await postgrest.from('messages').delete().eq('message', 'foo').execute(); - final resMsg = - await postgrest.from('messages').select().filter('message', 'eq', 'foo').execute(); + final resMsg = await postgrest + .from('messages') + .select() + .filter('message', 'eq', 'foo') + .execute(); expect(resMsg.data.length, 0); }); @@ -86,4 +100,74 @@ void main() { final res = await postgrest.from('user').select().execute(); expect(res.error.code, 'SocketException'); }); + + test('select with head:true', () async { + final res = await postgrest.from('users').select().execute(head: true); + expect(res.data, null); + }); + + test('select with head:true, count: exact', () async { + final res = await postgrest + .from('users') + .select() + .execute(head: true, count: CountOption.exact); + expect(res.data, null); + expect(res.count, 4); + }); + + test('select with count: planned', () async { + final res = await postgrest + .from('users') + .select() + .execute(count: CountOption.exact); + expect(res.count, const TypeMatcher()); + }); + + test('select with head:true, count: estimated', () async { + final res = await postgrest + .from('users') + .select() + .execute(count: CountOption.exact); + expect(res.count, const TypeMatcher()); + }); + + test('stored procedure with head: true', () async { + final res = + await postgrest.from('users').rpc('get_status').execute(head: true); + expect(res.data, null); + }); + + test('stored procedure with count: exact', () async { + final res = await postgrest + .from('users') + .rpc('get_status') + .execute(count: CountOption.exact); + expect(res.count, const TypeMatcher()); + }); + + test('insert with count: exact', () async { + final res = await postgrest.from('users').insert( + {'username': 'countexact', 'status': 'OFFLINE'}, + upsert: true, onConflict: 'username').execute(count: CountOption.exact); + expect(res.count, 1); + }); + + test('update with count: exact', () async { + final res = await postgrest + .from('users') + .update({'status': 'ONLINE'}) + .eq('username', 'countexact') + .execute(count: CountOption.exact); + expect(res.count, 1); + }); + + test('delete with count: exact', () async { + final res = await postgrest + .from('users') + .delete() + .eq('username', 'countexact') + .execute(count: CountOption.exact); + + expect(res.count, 1); + }); } diff --git a/test/resource_embedding_test.dart b/test/resource_embedding_test.dart index cff18bd..e68c7ea 100644 --- a/test/resource_embedding_test.dart +++ b/test/resource_embedding_test.dart @@ -16,8 +16,11 @@ void main() { }); test('embedded eq', () async { - final res = - await postgrest.from('users').select('messages(*)').eq('messages.channel_id', 1).execute(); + final res = await postgrest + .from('users') + .select('messages(*)') + .eq('messages.channel_id', 1) + .execute(); expect(res.data[0]['messages'].length, 1); expect(res.data[1]['messages'].length, 0); expect(res.data[2]['messages'].length, 0);