diff --git a/infra/postgrest/db/00-schema.sql b/infra/postgrest/db/00-schema.sql index a1a2b6fa..b40df949 100644 --- a/infra/postgrest/db/00-schema.sql +++ b/infra/postgrest/db/00-schema.sql @@ -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( @@ -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) -); \ No newline at end of file +); diff --git a/packages/postgrest/lib/src/postgrest.dart b/packages/postgrest/lib/src/postgrest.dart index 6bace201..9676854f 100644 --- a/packages/postgrest/lib/src/postgrest.dart +++ b/packages/postgrest/lib/src/postgrest.dart @@ -81,7 +81,17 @@ 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'}) @@ -89,6 +99,7 @@ class PostgrestClient { PostgrestFilterBuilder rpc( String fn, { Map? params, + bool get = false, }) { final url = '${this.url}/rpc/$fn'; return PostgrestRpcBuilder( @@ -97,7 +108,7 @@ class PostgrestClient { schema: _schema, httpClient: httpClient, isolate: _isolate, - ).rpc(params); + ).rpc(params, get); } Future dispose() async { diff --git a/packages/postgrest/lib/src/postgrest_builder.dart b/packages/postgrest/lib/src/postgrest_builder.dart index d7ef48e3..822ac03d 100644 --- a/packages/postgrest/lib/src/postgrest_builder.dart +++ b/packages/postgrest/lib/src/postgrest_builder.dart @@ -348,6 +348,15 @@ class PostgrestBuilder implements Future { 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 asStream() { final controller = StreamController.broadcast(); diff --git a/packages/postgrest/lib/src/postgrest_filter_builder.dart b/packages/postgrest/lib/src/postgrest_filter_builder.dart index a289f953..97798fe6 100644 --- a/packages/postgrest/lib/src/postgrest_filter_builder.dart +++ b/packages/postgrest/lib/src/postgrest_filter_builder.dart @@ -7,15 +7,6 @@ class PostgrestFilterBuilder extends PostgrestTransformBuilder { PostgrestFilterBuilder 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 diff --git a/packages/postgrest/lib/src/postgrest_rpc_builder.dart b/packages/postgrest/lib/src/postgrest_rpc_builder.dart index 49df9eaf..9f5190e5 100644 --- a/packages/postgrest/lib/src/postgrest_rpc_builder.dart +++ b/packages/postgrest/lib/src/postgrest_rpc_builder.dart @@ -17,12 +17,36 @@ class PostgrestRpcBuilder extends RawPostgrestBuilder { ), ); - /// Performs stored procedures on the database. + /// {@macro postgrest_rpc} PostgrestFilterBuilder rpc([ 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, )); } diff --git a/packages/postgrest/test/basic_test.dart b/packages/postgrest/test/basic_test.dart index 02f98275..a65f88bc 100644 --- a/packages/postgrest/test/basic_test.dart +++ b/packages/postgrest/test/basic_test.dart @@ -63,6 +63,29 @@ void main() { expect(res, isA()); }); + test('stored procedure with array parameter', () async { + final res = await postgrest.rpc( + '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( + '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'); @@ -448,10 +471,12 @@ void main() { }); }); group("Custom http client", () { + CustomHttpClient customHttpClient = CustomHttpClient(); setUp(() { + customHttpClient = CustomHttpClient(); postgrestCustomHttpClient = PostgrestClient( rootUrl, - httpClient: CustomHttpClient(), + httpClient: customHttpClient, ); }); @@ -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( + '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); } }); }); diff --git a/packages/postgrest/test/custom_http_client.dart b/packages/postgrest/test/custom_http_client.dart index f2da3983..7ecc4b81 100644 --- a/packages/postgrest/test/custom_http_client.dart +++ b/packages/postgrest/test/custom_http_client.dart @@ -1,10 +1,15 @@ +import 'dart:typed_data'; + import 'package:http/http.dart'; class CustomHttpClient extends BaseClient { BaseRequest? lastRequest; + Uint8List? lastBody; @override Future send(BaseRequest request) async { lastRequest = request; + final bodyStream = request.finalize(); + lastBody = await bodyStream.toBytes(); if (request.url.path.endsWith("empty-succ")) { return StreamedResponse( @@ -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, ); diff --git a/packages/postgrest/test/reset_helper.dart b/packages/postgrest/test/reset_helper.dart index 774d5044..456d68c3 100644 --- a/packages/postgrest/test/reset_helper.dart +++ b/packages/postgrest/test/reset_helper.dart @@ -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(); } diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index db4ca3b4..41b47794 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -200,13 +200,14 @@ class SupabaseClient { ); } - /// Perform a stored procedure call. + /// {@macro postgrest_rpc} PostgrestFilterBuilder rpc( String fn, { Map? 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. diff --git a/packages/supabase/lib/src/supabase_query_schema.dart b/packages/supabase/lib/src/supabase_query_schema.dart index 0b73fa2f..d40c4940 100644 --- a/packages/supabase/lib/src/supabase_query_schema.dart +++ b/packages/supabase/lib/src/supabase_query_schema.dart @@ -48,13 +48,18 @@ class SupabaseQuerySchema { ); } - /// Perform a stored procedure call. + /// {@macro postgrest_rpc} PostgrestFilterBuilder rpc( String fn, { Map? 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) {