Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Read-only access mode rpc #1081

Merged
merged 1 commit into from
Nov 11, 2024
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
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
Loading