Skip to content

Commit 8b0cbe5

Browse files
authored
Merge pull request #11 from supabase-community/feat/rpc
feat: Add support for mocking RPC functions.
2 parents a80881d + 211dcea commit 8b0cbe5

File tree

2 files changed

+458
-39
lines changed

2 files changed

+458
-39
lines changed

lib/src/mock_supabase_http_client.dart

Lines changed: 163 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,163 @@ import 'package:http/http.dart';
44

55
class MockSupabaseHttpClient extends BaseClient {
66
final Map<String, List<Map<String, dynamic>>> _database = {};
7+
final Map<
8+
String,
9+
dynamic Function(Map<String, dynamic>? params,
10+
Map<String, List<Map<String, dynamic>>> tables)> _rpcFunctions = {};
711

812
MockSupabaseHttpClient();
913

1014
void reset() {
11-
// Clear the mock database
15+
// Clear the mock database and RPC functions
1216
_database.clear();
17+
_rpcFunctions.clear();
18+
}
19+
20+
/// Registers a RPC function that can be called using the `rpc` method on a `Postgrest` client.
21+
///
22+
/// [name] is the name of the RPC function.
23+
///
24+
/// Pass the function definition of the RPC to [function]. Use the following parameters:
25+
///
26+
/// [params] contains the parameters passed to the RPC function.
27+
///
28+
/// [tables] contains the mock database. It's a `Map<String, List<Map<String, dynamic>>>`
29+
/// where the key is `[schema].[table]` and the value is a list of rows in the table.
30+
/// Use it when you need to mock a RPC function that needs to modify the data in your database.
31+
///
32+
/// Example value of `tables`:
33+
/// ```dart
34+
/// {
35+
/// 'public.users': [
36+
/// {'id': 1, 'name': 'Alice', 'email': '[email protected]'},
37+
/// {'id': 2, 'name': 'Bob', 'email': '[email protected]'},
38+
/// ],
39+
/// 'public.posts': [
40+
/// {'id': 1, 'title': 'First post', 'user_id': 1},
41+
/// {'id': 2, 'title': 'Second post', 'user_id': 2},
42+
/// ],
43+
/// }
44+
/// ```
45+
///
46+
/// Example of registering a RPC function:
47+
/// ```dart
48+
/// mockSupabaseHttpClient.registerRpcFunction(
49+
/// 'get_status',
50+
/// (params, tables) => {'status': 'ok'},
51+
/// );
52+
///
53+
/// final mockSupabase = SupabaseClient(
54+
/// 'https://mock.supabase.co',
55+
/// 'fakeAnonKey',
56+
/// httpClient: mockSupabaseHttpClient,
57+
/// );
58+
///
59+
/// mockSupabase.rpc('get_status').select(); // returns {'status': 'ok'}
60+
/// ```
61+
///
62+
/// Example of an RPC function that modifies the data in the database:
63+
/// ```dart
64+
/// mockSupabaseHttpClient.registerRpcFunction(
65+
/// 'update_post_title',
66+
/// (params, tables) {
67+
/// final postId = params!['id'] as int;
68+
/// final newTitle = params!['title'] as String;
69+
/// final post = tables['public.posts']!.firstWhere((post) => post['id'] == postId);
70+
/// post['title'] = newTitle;
71+
/// },
72+
/// );
73+
///
74+
/// final mockSupabase = SupabaseClient(
75+
/// 'https://mock.supabase.co',
76+
/// 'fakeAnonKey',
77+
/// httpClient: mockSupabaseHttpClient,
78+
/// );
79+
///
80+
/// // Insert initial data
81+
/// await mockSupabase.from('posts').insert([
82+
/// {'id': 1, 'title': 'Old title'},
83+
/// ]);
84+
///
85+
/// // Call the RPC function
86+
/// await mockSupabase.rpc('update_post_title', params: {'id': 1, 'title': 'New title'});
87+
///
88+
/// // Verify that the post was modified
89+
/// final posts = await mockSupabase.from('posts').select().eq('id', 1);
90+
/// expect(posts.first['title'], 'New title');
91+
/// ```
92+
void registerRpcFunction(
93+
String name,
94+
dynamic Function(Map<String, dynamic>? params,
95+
Map<String, List<Map<String, dynamic>>> tables)
96+
function) {
97+
_rpcFunctions[name] = function;
1398
}
1499

15100
@override
16101
Future<StreamedResponse> send(BaseRequest request) async {
17-
// Extract the table name from the URL
18-
final tableName = _extractTableName(
19-
url: request.url,
20-
headers: request.headers,
21-
method: request.method,
22-
);
102+
// Decode the request body if it's not a GET, DELETE, or HEAD request
103+
dynamic body;
104+
if (request.method != 'GET' &&
105+
request.method != 'DELETE' &&
106+
request.method != 'HEAD' &&
107+
request is Request) {
108+
final String requestBody =
109+
await request.finalize().transform(utf8.decoder).join();
110+
if (requestBody.isNotEmpty) {
111+
body = jsonDecode(requestBody);
112+
}
113+
}
23114

24-
// Decode the request body if it's not a GET request
25-
final body = (request.method != 'GET' &&
26-
request.method != 'DELETE' &&
27-
request.method != 'HEAD') &&
28-
request is Request
29-
? jsonDecode(await request.finalize().transform(utf8.decoder).join())
30-
: null;
31-
32-
// Handle different HTTP methods
33-
switch (request.method) {
34-
case 'POST':
35-
// Handle upsert if the Prefer header is set
36-
final preferHeader = request.headers['Prefer'];
37-
if (preferHeader != null &&
38-
preferHeader.contains('resolution=merge-duplicates')) {
39-
return _handleUpsert(tableName, body, request);
115+
// Extract the table name or RPC function name from the URL
116+
final pathSegments = request.url.pathSegments;
117+
final restIndex = pathSegments.indexOf('v1');
118+
if (restIndex != -1 && restIndex < pathSegments.length - 1) {
119+
final resourceName = pathSegments[restIndex + 1];
120+
121+
if (resourceName == 'rpc') {
122+
// Handle RPC call
123+
if (pathSegments.length > restIndex + 2) {
124+
final functionName = pathSegments[restIndex + 2];
125+
return _handleRpc(functionName, request, body);
126+
} else {
127+
return _createResponse({'error': 'RPC function name not provided'},
128+
statusCode: 400, request: request);
129+
}
130+
} else {
131+
// Handle regular database operations
132+
final tableName = _extractTableName(
133+
url: request.url,
134+
headers: request.headers,
135+
method: request.method,
136+
);
137+
138+
// Handle different HTTP methods
139+
switch (request.method) {
140+
case 'POST':
141+
// Handle upsert if the Prefer header is set
142+
final preferHeader = request.headers['Prefer'];
143+
if (preferHeader != null &&
144+
preferHeader.contains('resolution=merge-duplicates')) {
145+
return _handleUpsert(tableName, body, request);
146+
}
147+
return _handleInsert(tableName, body, request);
148+
case 'PATCH':
149+
return _handleUpdate(tableName, body, request);
150+
case 'DELETE':
151+
return _handleDelete(tableName, body, request);
152+
case 'GET':
153+
return _handleSelect(
154+
tableName, request.url.queryParameters, request);
155+
case 'HEAD':
156+
return _handleHead(tableName, request.url.queryParameters, request);
157+
default:
158+
return _createResponse({'error': 'Method not allowed'},
159+
statusCode: 405, request: request);
40160
}
41-
return _handleInsert(tableName, body, request);
42-
case 'PATCH':
43-
return _handleUpdate(tableName, body, request);
44-
case 'DELETE':
45-
return _handleDelete(tableName, body, request);
46-
case 'GET':
47-
return _handleSelect(tableName, request.url.queryParameters, request);
48-
case 'HEAD':
49-
return _handleHead(tableName, request.url.queryParameters, request);
50-
default:
51-
return _createResponse({'error': 'Method not allowed'},
52-
statusCode: 405, request: request);
161+
}
53162
}
163+
throw Exception('Invalid URL format: unable to extract table name');
54164
}
55165

56166
String _extractTableName({
@@ -706,4 +816,22 @@ class MockSupabaseHttpClient extends BaseClient {
706816
request: request,
707817
);
708818
}
819+
820+
StreamedResponse _handleRpc(
821+
String functionName, BaseRequest request, dynamic body) {
822+
if (!_rpcFunctions.containsKey(functionName)) {
823+
return _createResponse({'error': 'RPC function not found'},
824+
statusCode: 404, request: request);
825+
}
826+
827+
final function = _rpcFunctions[functionName]!;
828+
829+
try {
830+
final result = function(body, _database);
831+
return _createResponse(result, request: request);
832+
} catch (e) {
833+
return _createResponse({'error': 'RPC function execution failed: $e'},
834+
statusCode: 500, request: request);
835+
}
836+
}
709837
}

0 commit comments

Comments
 (0)