@@ -4,53 +4,163 @@ import 'package:http/http.dart';
4
4
5
5
class MockSupabaseHttpClient extends BaseClient {
6
6
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 = {};
7
11
8
12
MockSupabaseHttpClient ();
9
13
10
14
void reset () {
11
- // Clear the mock database
15
+ // Clear the mock database and RPC functions
12
16
_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;
13
98
}
14
99
15
100
@override
16
101
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
+ }
23
114
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);
40
160
}
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
+ }
53
162
}
163
+ throw Exception ('Invalid URL format: unable to extract table name' );
54
164
}
55
165
56
166
String _extractTableName ({
@@ -706,4 +816,22 @@ class MockSupabaseHttpClient extends BaseClient {
706
816
request: request,
707
817
);
708
818
}
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
+ }
709
837
}
0 commit comments