@@ -4,53 +4,163 @@ import 'package:http/http.dart';
44
55class 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