Skip to content

Commit

Permalink
feat: Read-only access mode rpc (#1081)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vinzent03 authored Nov 11, 2024
1 parent 7e7bc0c commit d0a0415
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 21 deletions.
9 changes: 8 additions & 1 deletion infra/postgrest/db/00-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ CREATE FUNCTION public.get_integer()
End;
$$ LANGUAGE plpgsql;

CREATE FUNCTION public.get_array_element(arr integer[], index integer)
RETURNS integer AS $$
BEGIN
RETURN arr[index];
END;
$$ LANGUAGE plpgsql;

-- SECOND SCHEMA USERS
CREATE TYPE personal.user_status AS ENUM ('ONLINE', 'OFFLINE');
CREATE TABLE personal.users(
Expand Down Expand Up @@ -103,4 +110,4 @@ CREATE TABLE public.addresses (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username text REFERENCES users NOT NULL,
location geometry(POINT,4326)
);
);
15 changes: 13 additions & 2 deletions packages/postgrest/lib/src/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,25 @@ class PostgrestClient {
);
}

/// Perform a stored procedure call.
/// {@template postgrest_rpc}
/// Performs a stored procedure call.
///
/// [fn] is the name of the function to call.
///
/// [params] is an optinal object to pass as arguments to the function call.
///
/// When [get] is set to `true`, the function will be called with read-only
/// access mode.
///
/// {@endtemplate}
///
/// ```dart
/// supabase.rpc('get_status', params: {'name_param': 'supabot'})
/// ```
PostgrestFilterBuilder<T> rpc<T>(
String fn, {
Map? params,
bool get = false,
}) {
final url = '${this.url}/rpc/$fn';
return PostgrestRpcBuilder(
Expand All @@ -97,7 +108,7 @@ class PostgrestClient {
schema: _schema,
httpClient: httpClient,
isolate: _isolate,
).rpc(params);
).rpc(params, get);
}

Future<void> dispose() async {
Expand Down
9 changes: 9 additions & 0 deletions packages/postgrest/lib/src/postgrest_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,15 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
return _url.replace(queryParameters: searchParams);
}

/// Convert list filter to query params string
String _cleanFilterArray(List filter) {
if (filter.every((element) => element is num)) {
return filter.map((s) => '$s').join(',');
} else {
return filter.map((s) => '"$s"').join(',');
}
}

@override
Stream<T> asStream() {
final controller = StreamController<T>.broadcast();
Expand Down
9 changes: 0 additions & 9 deletions packages/postgrest/lib/src/postgrest_filter_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@ class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
PostgrestFilterBuilder<T> copyWithUrl(Uri url) =>
PostgrestFilterBuilder(_copyWith(url: url));

/// Convert list filter to query params string
String _cleanFilterArray(List filter) {
if (filter.every((element) => element is num)) {
return filter.map((s) => '$s').join(',');
} else {
return filter.map((s) => '"$s"').join(',');
}
}

/// Finds all rows which doesn't satisfy the filter.
///
/// ```dart
Expand Down
28 changes: 26 additions & 2 deletions packages/postgrest/lib/src/postgrest_rpc_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,36 @@ class PostgrestRpcBuilder extends RawPostgrestBuilder {
),
);

/// Performs stored procedures on the database.
/// {@macro postgrest_rpc}
PostgrestFilterBuilder<T> rpc<T>([
Object? params,
bool get = false,
]) {
var newUrl = _url;
final String method;
if (get) {
method = METHOD_GET;
if (params is Map) {
for (final entry in params.entries) {
assert(entry.key is String,
"RPC params map keys must be of type String");

final MapEntry(:key, :value) = entry;
final formattedValue =
value is List ? '{${_cleanFilterArray(value)}}' : value;
newUrl =
appendSearchParams(key.toString(), '$formattedValue', newUrl);
}
} else {
throw ArgumentError.value(params, 'params', 'argument must be a Map');
}
} else {
method = METHOD_POST;
}

return PostgrestFilterBuilder(_copyWithType(
method: METHOD_POST,
method: method,
url: newUrl,
body: params,
));
}
Expand Down
44 changes: 43 additions & 1 deletion packages/postgrest/test/basic_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ void main() {
expect(res, isA<int>());
});

test('stored procedure with array parameter', () async {
final res = await postgrest.rpc<int>(
'get_array_element',
params: {
'arr': [37, 420, 64],
'index': 2
},
);
expect(res, 420);
});

test('stored procedure with read-only access mode', () async {
final res = await postgrest.rpc<int>(
'get_array_element',
params: {
'arr': [37, 420, 64],
'index': 2
},
get: true,
);
expect(res, 420);
});

test('custom headers', () async {
final postgrest = PostgrestClient(rootUrl, headers: {'apikey': 'foo'});
expect(postgrest.headers['apikey'], 'foo');
Expand Down Expand Up @@ -448,10 +471,12 @@ void main() {
});
});
group("Custom http client", () {
CustomHttpClient customHttpClient = CustomHttpClient();
setUp(() {
customHttpClient = CustomHttpClient();
postgrestCustomHttpClient = PostgrestClient(
rootUrl,
httpClient: CustomHttpClient(),
httpClient: customHttpClient,
);
});

Expand Down Expand Up @@ -486,6 +511,23 @@ void main() {
'Stored procedure was able to be called, even tho it does not exist');
} on PostgrestException catch (error) {
expect(error.code, '420');
expect(customHttpClient.lastRequest?.method, "POST");
}
});

test('stored procedure call in read-only access mode', () async {
try {
await postgrestCustomHttpClient.rpc<String>(
'get_status',
params: {'name_param': 'supabot'},
get: true,
);
fail(
'Stored procedure was able to be called, even tho it does not exist');
} on PostgrestException catch (error) {
expect(error.code, '420');
expect(customHttpClient.lastRequest?.method, "GET");
expect(customHttpClient.lastBody, isEmpty);
}
});
});
Expand Down
7 changes: 6 additions & 1 deletion packages/postgrest/test/custom_http_client.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import 'dart:typed_data';

import 'package:http/http.dart';

class CustomHttpClient extends BaseClient {
BaseRequest? lastRequest;
Uint8List? lastBody;
@override
Future<StreamedResponse> send(BaseRequest request) async {
lastRequest = request;
final bodyStream = request.finalize();
lastBody = await bodyStream.toBytes();

if (request.url.path.endsWith("empty-succ")) {
return StreamedResponse(
Expand All @@ -15,7 +20,7 @@ class CustomHttpClient extends BaseClient {
}
//Return custom status code to check for usage of this client.
return StreamedResponse(
request.finalize(),
Stream.value(lastBody!),
420,
request: request,
);
Expand Down
1 change: 0 additions & 1 deletion packages/postgrest/test/reset_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class ResetHelper {
_users = (await _postgrest.from('users').select());
_channels = await _postgrest.from('channels').select();
_messages = await _postgrest.from('messages').select();
print('messages has ${_messages.length} items');
_reactions = await _postgrest.from('reactions').select();
_addresses = await _postgrest.from('addresses').select();
}
Expand Down
5 changes: 3 additions & 2 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,14 @@ class SupabaseClient {
);
}

/// Perform a stored procedure call.
/// {@macro postgrest_rpc}
PostgrestFilterBuilder<T> rpc<T>(
String fn, {
Map<String, dynamic>? params,
get = false,
}) {
rest.headers.addAll({...rest.headers, ...headers});
return rest.rpc(fn, params: params);
return rest.rpc(fn, params: params, get: get);
}

/// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
Expand Down
9 changes: 7 additions & 2 deletions packages/supabase/lib/src/supabase_query_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@ class SupabaseQuerySchema {
);
}

/// Perform a stored procedure call.
/// {@macro postgrest_rpc}
PostgrestFilterBuilder<T> rpc<T>(
String fn, {
Map<String, dynamic>? params,
bool get = false,
}) {
_rest.headers.addAll({..._rest.headers, ..._headers});
return _rest.rpc(fn, params: params);
return _rest.rpc(
fn,
params: params,
get: get,
);
}

SupabaseQuerySchema schema(String schema) {
Expand Down

0 comments on commit d0a0415

Please sign in to comment.