Skip to content

feat: Add support for mocking RPC functions. #11

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

Merged
merged 3 commits into from
Oct 25, 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
198 changes: 163 additions & 35 deletions lib/src/mock_supabase_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,163 @@ import 'package:http/http.dart';

class MockSupabaseHttpClient extends BaseClient {
final Map<String, List<Map<String, dynamic>>> _database = {};
final Map<
String,
dynamic Function(Map<String, dynamic>? params,
Map<String, List<Map<String, dynamic>>> tables)> _rpcFunctions = {};

MockSupabaseHttpClient();

void reset() {
// Clear the mock database
// Clear the mock database and RPC functions
_database.clear();
_rpcFunctions.clear();
}

/// Registers a RPC function that can be called using the `rpc` method on a `Postgrest` client.
///
/// [name] is the name of the RPC function.
///
/// Pass the function definition of the RPC to [function]. Use the following parameters:
///
/// [params] contains the parameters passed to the RPC function.
///
/// [tables] contains the mock database. It's a `Map<String, List<Map<String, dynamic>>>`
/// where the key is `[schema].[table]` and the value is a list of rows in the table.
/// Use it when you need to mock a RPC function that needs to modify the data in your database.
///
/// Example value of `tables`:
/// ```dart
/// {
/// 'public.users': [
/// {'id': 1, 'name': 'Alice', 'email': '[email protected]'},
/// {'id': 2, 'name': 'Bob', 'email': '[email protected]'},
/// ],
/// 'public.posts': [
/// {'id': 1, 'title': 'First post', 'user_id': 1},
/// {'id': 2, 'title': 'Second post', 'user_id': 2},
/// ],
/// }
/// ```
///
/// Example of registering a RPC function:
/// ```dart
/// mockSupabaseHttpClient.registerRpcFunction(
/// 'get_status',
/// (params, tables) => {'status': 'ok'},
/// );
///
/// final mockSupabase = SupabaseClient(
/// 'https://mock.supabase.co',
/// 'fakeAnonKey',
/// httpClient: mockSupabaseHttpClient,
/// );
///
/// mockSupabase.rpc('get_status').select(); // returns {'status': 'ok'}
/// ```
///
/// Example of an RPC function that modifies the data in the database:
/// ```dart
/// mockSupabaseHttpClient.registerRpcFunction(
/// 'update_post_title',
/// (params, tables) {
/// final postId = params!['id'] as int;
/// final newTitle = params!['title'] as String;
/// final post = tables['public.posts']!.firstWhere((post) => post['id'] == postId);
/// post['title'] = newTitle;
/// },
/// );
///
/// final mockSupabase = SupabaseClient(
/// 'https://mock.supabase.co',
/// 'fakeAnonKey',
/// httpClient: mockSupabaseHttpClient,
/// );
///
/// // Insert initial data
/// await mockSupabase.from('posts').insert([
/// {'id': 1, 'title': 'Old title'},
/// ]);
///
/// // Call the RPC function
/// await mockSupabase.rpc('update_post_title', params: {'id': 1, 'title': 'New title'});
///
/// // Verify that the post was modified
/// final posts = await mockSupabase.from('posts').select().eq('id', 1);
/// expect(posts.first['title'], 'New title');
/// ```
void registerRpcFunction(
String name,
dynamic Function(Map<String, dynamic>? params,
Map<String, List<Map<String, dynamic>>> tables)
function) {
_rpcFunctions[name] = function;
}

@override
Future<StreamedResponse> send(BaseRequest request) async {
// Extract the table name from the URL
final tableName = _extractTableName(
url: request.url,
headers: request.headers,
method: request.method,
);
// Decode the request body if it's not a GET, DELETE, or HEAD request
dynamic body;
if (request.method != 'GET' &&
request.method != 'DELETE' &&
request.method != 'HEAD' &&
request is Request) {
final String requestBody =
await request.finalize().transform(utf8.decoder).join();
if (requestBody.isNotEmpty) {
body = jsonDecode(requestBody);
}
}

// Decode the request body if it's not a GET request
final body = (request.method != 'GET' &&
request.method != 'DELETE' &&
request.method != 'HEAD') &&
request is Request
? jsonDecode(await request.finalize().transform(utf8.decoder).join())
: null;

// Handle different HTTP methods
switch (request.method) {
case 'POST':
// Handle upsert if the Prefer header is set
final preferHeader = request.headers['Prefer'];
if (preferHeader != null &&
preferHeader.contains('resolution=merge-duplicates')) {
return _handleUpsert(tableName, body, request);
// Extract the table name or RPC function name from the URL
final pathSegments = request.url.pathSegments;
final restIndex = pathSegments.indexOf('v1');
if (restIndex != -1 && restIndex < pathSegments.length - 1) {
final resourceName = pathSegments[restIndex + 1];

if (resourceName == 'rpc') {
// Handle RPC call
if (pathSegments.length > restIndex + 2) {
final functionName = pathSegments[restIndex + 2];
return _handleRpc(functionName, request, body);
} else {
return _createResponse({'error': 'RPC function name not provided'},
statusCode: 400, request: request);
}
} else {
// Handle regular database operations
final tableName = _extractTableName(
url: request.url,
headers: request.headers,
method: request.method,
);

// Handle different HTTP methods
switch (request.method) {
case 'POST':
// Handle upsert if the Prefer header is set
final preferHeader = request.headers['Prefer'];
if (preferHeader != null &&
preferHeader.contains('resolution=merge-duplicates')) {
return _handleUpsert(tableName, body, request);
}
return _handleInsert(tableName, body, request);
case 'PATCH':
return _handleUpdate(tableName, body, request);
case 'DELETE':
return _handleDelete(tableName, body, request);
case 'GET':
return _handleSelect(
tableName, request.url.queryParameters, request);
case 'HEAD':
return _handleHead(tableName, request.url.queryParameters, request);
default:
return _createResponse({'error': 'Method not allowed'},
statusCode: 405, request: request);
}
return _handleInsert(tableName, body, request);
case 'PATCH':
return _handleUpdate(tableName, body, request);
case 'DELETE':
return _handleDelete(tableName, body, request);
case 'GET':
return _handleSelect(tableName, request.url.queryParameters, request);
case 'HEAD':
return _handleHead(tableName, request.url.queryParameters, request);
default:
return _createResponse({'error': 'Method not allowed'},
statusCode: 405, request: request);
}
}
throw Exception('Invalid URL format: unable to extract table name');
}

String _extractTableName({
Expand Down Expand Up @@ -706,4 +816,22 @@ class MockSupabaseHttpClient extends BaseClient {
request: request,
);
}

StreamedResponse _handleRpc(
String functionName, BaseRequest request, dynamic body) {
if (!_rpcFunctions.containsKey(functionName)) {
return _createResponse({'error': 'RPC function not found'},
statusCode: 404, request: request);
}

final function = _rpcFunctions[functionName]!;

try {
final result = function(body, _database);
return _createResponse(result, request: request);
} catch (e) {
return _createResponse({'error': 'RPC function execution failed: $e'},
statusCode: 500, request: request);
}
}
}
Loading