Skip to content

Commit d0a0415

Browse files
authored
feat: Read-only access mode rpc (#1081)
1 parent 7e7bc0c commit d0a0415

File tree

10 files changed

+115
-21
lines changed

10 files changed

+115
-21
lines changed

infra/postgrest/db/00-schema.sql

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ CREATE FUNCTION public.get_integer()
6868
End;
6969
$$ LANGUAGE plpgsql;
7070

71+
CREATE FUNCTION public.get_array_element(arr integer[], index integer)
72+
RETURNS integer AS $$
73+
BEGIN
74+
RETURN arr[index];
75+
END;
76+
$$ LANGUAGE plpgsql;
77+
7178
-- SECOND SCHEMA USERS
7279
CREATE TYPE personal.user_status AS ENUM ('ONLINE', 'OFFLINE');
7380
CREATE TABLE personal.users(
@@ -103,4 +110,4 @@ CREATE TABLE public.addresses (
103110
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
104111
username text REFERENCES users NOT NULL,
105112
location geometry(POINT,4326)
106-
);
113+
);

packages/postgrest/lib/src/postgrest.dart

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,25 @@ class PostgrestClient {
8181
);
8282
}
8383

84-
/// Perform a stored procedure call.
84+
/// {@template postgrest_rpc}
85+
/// Performs a stored procedure call.
86+
///
87+
/// [fn] is the name of the function to call.
88+
///
89+
/// [params] is an optinal object to pass as arguments to the function call.
90+
///
91+
/// When [get] is set to `true`, the function will be called with read-only
92+
/// access mode.
93+
///
94+
/// {@endtemplate}
8595
///
8696
/// ```dart
8797
/// supabase.rpc('get_status', params: {'name_param': 'supabot'})
8898
/// ```
8999
PostgrestFilterBuilder<T> rpc<T>(
90100
String fn, {
91101
Map? params,
102+
bool get = false,
92103
}) {
93104
final url = '${this.url}/rpc/$fn';
94105
return PostgrestRpcBuilder(
@@ -97,7 +108,7 @@ class PostgrestClient {
97108
schema: _schema,
98109
httpClient: httpClient,
99110
isolate: _isolate,
100-
).rpc(params);
111+
).rpc(params, get);
101112
}
102113

103114
Future<void> dispose() async {

packages/postgrest/lib/src/postgrest_builder.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,15 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
348348
return _url.replace(queryParameters: searchParams);
349349
}
350350

351+
/// Convert list filter to query params string
352+
String _cleanFilterArray(List filter) {
353+
if (filter.every((element) => element is num)) {
354+
return filter.map((s) => '$s').join(',');
355+
} else {
356+
return filter.map((s) => '"$s"').join(',');
357+
}
358+
}
359+
351360
@override
352361
Stream<T> asStream() {
353362
final controller = StreamController<T>.broadcast();

packages/postgrest/lib/src/postgrest_filter_builder.dart

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,6 @@ class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
77
PostgrestFilterBuilder<T> copyWithUrl(Uri url) =>
88
PostgrestFilterBuilder(_copyWith(url: url));
99

10-
/// Convert list filter to query params string
11-
String _cleanFilterArray(List filter) {
12-
if (filter.every((element) => element is num)) {
13-
return filter.map((s) => '$s').join(',');
14-
} else {
15-
return filter.map((s) => '"$s"').join(',');
16-
}
17-
}
18-
1910
/// Finds all rows which doesn't satisfy the filter.
2011
///
2112
/// ```dart

packages/postgrest/lib/src/postgrest_rpc_builder.dart

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,36 @@ class PostgrestRpcBuilder extends RawPostgrestBuilder {
1717
),
1818
);
1919

20-
/// Performs stored procedures on the database.
20+
/// {@macro postgrest_rpc}
2121
PostgrestFilterBuilder<T> rpc<T>([
2222
Object? params,
23+
bool get = false,
2324
]) {
25+
var newUrl = _url;
26+
final String method;
27+
if (get) {
28+
method = METHOD_GET;
29+
if (params is Map) {
30+
for (final entry in params.entries) {
31+
assert(entry.key is String,
32+
"RPC params map keys must be of type String");
33+
34+
final MapEntry(:key, :value) = entry;
35+
final formattedValue =
36+
value is List ? '{${_cleanFilterArray(value)}}' : value;
37+
newUrl =
38+
appendSearchParams(key.toString(), '$formattedValue', newUrl);
39+
}
40+
} else {
41+
throw ArgumentError.value(params, 'params', 'argument must be a Map');
42+
}
43+
} else {
44+
method = METHOD_POST;
45+
}
46+
2447
return PostgrestFilterBuilder(_copyWithType(
25-
method: METHOD_POST,
48+
method: method,
49+
url: newUrl,
2650
body: params,
2751
));
2852
}

packages/postgrest/test/basic_test.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ void main() {
6363
expect(res, isA<int>());
6464
});
6565

66+
test('stored procedure with array parameter', () async {
67+
final res = await postgrest.rpc<int>(
68+
'get_array_element',
69+
params: {
70+
'arr': [37, 420, 64],
71+
'index': 2
72+
},
73+
);
74+
expect(res, 420);
75+
});
76+
77+
test('stored procedure with read-only access mode', () async {
78+
final res = await postgrest.rpc<int>(
79+
'get_array_element',
80+
params: {
81+
'arr': [37, 420, 64],
82+
'index': 2
83+
},
84+
get: true,
85+
);
86+
expect(res, 420);
87+
});
88+
6689
test('custom headers', () async {
6790
final postgrest = PostgrestClient(rootUrl, headers: {'apikey': 'foo'});
6891
expect(postgrest.headers['apikey'], 'foo');
@@ -448,10 +471,12 @@ void main() {
448471
});
449472
});
450473
group("Custom http client", () {
474+
CustomHttpClient customHttpClient = CustomHttpClient();
451475
setUp(() {
476+
customHttpClient = CustomHttpClient();
452477
postgrestCustomHttpClient = PostgrestClient(
453478
rootUrl,
454-
httpClient: CustomHttpClient(),
479+
httpClient: customHttpClient,
455480
);
456481
});
457482

@@ -486,6 +511,23 @@ void main() {
486511
'Stored procedure was able to be called, even tho it does not exist');
487512
} on PostgrestException catch (error) {
488513
expect(error.code, '420');
514+
expect(customHttpClient.lastRequest?.method, "POST");
515+
}
516+
});
517+
518+
test('stored procedure call in read-only access mode', () async {
519+
try {
520+
await postgrestCustomHttpClient.rpc<String>(
521+
'get_status',
522+
params: {'name_param': 'supabot'},
523+
get: true,
524+
);
525+
fail(
526+
'Stored procedure was able to be called, even tho it does not exist');
527+
} on PostgrestException catch (error) {
528+
expect(error.code, '420');
529+
expect(customHttpClient.lastRequest?.method, "GET");
530+
expect(customHttpClient.lastBody, isEmpty);
489531
}
490532
});
491533
});

packages/postgrest/test/custom_http_client.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import 'dart:typed_data';
2+
13
import 'package:http/http.dart';
24

35
class CustomHttpClient extends BaseClient {
46
BaseRequest? lastRequest;
7+
Uint8List? lastBody;
58
@override
69
Future<StreamedResponse> send(BaseRequest request) async {
710
lastRequest = request;
11+
final bodyStream = request.finalize();
12+
lastBody = await bodyStream.toBytes();
813

914
if (request.url.path.endsWith("empty-succ")) {
1015
return StreamedResponse(
@@ -15,7 +20,7 @@ class CustomHttpClient extends BaseClient {
1520
}
1621
//Return custom status code to check for usage of this client.
1722
return StreamedResponse(
18-
request.finalize(),
23+
Stream.value(lastBody!),
1924
420,
2025
request: request,
2126
);

packages/postgrest/test/reset_helper.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class ResetHelper {
1515
_users = (await _postgrest.from('users').select());
1616
_channels = await _postgrest.from('channels').select();
1717
_messages = await _postgrest.from('messages').select();
18-
print('messages has ${_messages.length} items');
1918
_reactions = await _postgrest.from('reactions').select();
2019
_addresses = await _postgrest.from('addresses').select();
2120
}

packages/supabase/lib/src/supabase_client.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,14 @@ class SupabaseClient {
200200
);
201201
}
202202

203-
/// Perform a stored procedure call.
203+
/// {@macro postgrest_rpc}
204204
PostgrestFilterBuilder<T> rpc<T>(
205205
String fn, {
206206
Map<String, dynamic>? params,
207+
get = false,
207208
}) {
208209
rest.headers.addAll({...rest.headers, ...headers});
209-
return rest.rpc(fn, params: params);
210+
return rest.rpc(fn, params: params, get: get);
210211
}
211212

212213
/// Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.

packages/supabase/lib/src/supabase_query_schema.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,18 @@ class SupabaseQuerySchema {
4848
);
4949
}
5050

51-
/// Perform a stored procedure call.
51+
/// {@macro postgrest_rpc}
5252
PostgrestFilterBuilder<T> rpc<T>(
5353
String fn, {
5454
Map<String, dynamic>? params,
55+
bool get = false,
5556
}) {
5657
_rest.headers.addAll({..._rest.headers, ..._headers});
57-
return _rest.rpc(fn, params: params);
58+
return _rest.rpc(
59+
fn,
60+
params: params,
61+
get: get,
62+
);
5863
}
5964

6065
SupabaseQuerySchema schema(String schema) {

0 commit comments

Comments
 (0)