Skip to content
This repository was archived by the owner on May 13, 2023. It is now read-only.

Add head and count option #16

Merged
merged 8 commits into from
Jan 25, 2021
Merged
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
6 changes: 4 additions & 2 deletions lib/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class PostgrestClient {
/// new PostgrestClient(REST_URL)
/// new PostgrestClient(REST_URL, headers: { 'apikey': 'foo' })
/// ```
PostgrestClient(this.url, {Map<String, String> headers, this.schema}) : headers = headers ?? {};
PostgrestClient(this.url, {Map<String, String> headers, this.schema})
: headers = headers ?? {};

/// Authenticates the request with JWT.
PostgrestClient auth(String token) {
Expand All @@ -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);
}
}
62 changes: 50 additions & 12 deletions lib/src/builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<PostgrestResponse> execute() async {
Future<PostgrestResponse> 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;
Expand All @@ -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);
Expand All @@ -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,
);
}
}
Expand All @@ -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<String, String> headers, String schema}) {
PostgrestQueryBuilder(String url,
{Map<String, String> headers, String schema}) {
this.url = Uri.parse(url);
this.headers = headers ?? {};
this.schema = schema;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -225,7 +262,8 @@ class PostgrestTransformBuilder<T> 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');
Expand Down
12 changes: 12 additions & 0 deletions lib/src/count_option.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 6 additions & 1 deletion lib/src/postgrest_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@ 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<String, dynamic> json) => PostgrestResponse(
factory PostgrestResponse.fromJson(Map<String, dynamic> 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<String, dynamic>),
count: json['count'] as int,
);

Map<String, dynamic> toJson() => {
'data': data,
'status': status,
'statusText': statusText,
'error': error?.toJson(),
'count': count,
};
}
102 changes: 93 additions & 9 deletions test/basic_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:postgrest/src/count_option.dart';
import 'package:test/test.dart';
import 'package:postgrest/postgrest.dart';

Expand All @@ -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');
});

Expand All @@ -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 {
Expand All @@ -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');
});
Expand All @@ -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);
});

Expand All @@ -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<int>());
});

test('select with head:true, count: estimated', () async {
final res = await postgrest
.from('users')
.select()
.execute(count: CountOption.exact);
expect(res.count, const TypeMatcher<int>());
});

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<int>());
});

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);
});
}
7 changes: 5 additions & 2 deletions test/resource_embedding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down