Skip to content

Commit 96b96cc

Browse files
committed
Created mock_supabase_database for mocking rpc
1 parent a80881d commit 96b96cc

File tree

4 files changed

+551
-35
lines changed

4 files changed

+551
-35
lines changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/// A class that provides a Supabase-like interface for manipulating mock data
2+
class MockSupabaseDatabase {
3+
final Map<String, List<Map<String, dynamic>>> _database;
4+
5+
MockSupabaseDatabase(this._database);
6+
7+
/// Creates a query builder for the specified table
8+
MockSupabaseQueryBuilder from(String table) {
9+
return MockSupabaseQueryBuilder(_database, table);
10+
}
11+
}
12+
13+
/// A query builder that provides Supabase-like methods for querying and
14+
/// manipulating data
15+
class MockSupabaseQueryBuilder {
16+
final Map<String, List<Map<String, dynamic>>> _database;
17+
final String _table;
18+
final Map<String, String> _filters = {};
19+
int? _limitValue;
20+
int? _offsetValue;
21+
// Replace single order column and ascending flag with a list of order conditions
22+
final List<({String column, bool ascending})> _orderClauses = [];
23+
24+
MockSupabaseQueryBuilder(this._database, this._table);
25+
26+
/// Filters rows where [column] equals [value]
27+
MockSupabaseQueryBuilder eq(String column, dynamic value) {
28+
_filters[column] = 'eq.$value';
29+
return this;
30+
}
31+
32+
/// Filters rows where [column] does not equal [value]
33+
MockSupabaseQueryBuilder neq(String column, dynamic value) {
34+
_filters[column] = 'neq.$value';
35+
return this;
36+
}
37+
38+
/// Filters rows where [column] is greater than [value]
39+
MockSupabaseQueryBuilder gt(String column, dynamic value) {
40+
_filters[column] = 'gt.$value';
41+
return this;
42+
}
43+
44+
/// Filters rows where [column] is less than [value]
45+
MockSupabaseQueryBuilder lt(String column, dynamic value) {
46+
_filters[column] = 'lt.$value';
47+
return this;
48+
}
49+
50+
/// Filters rows where [column] is greater than or equal to [value]
51+
MockSupabaseQueryBuilder gte(String column, dynamic value) {
52+
_filters[column] = 'gte.$value';
53+
return this;
54+
}
55+
56+
/// Filters rows where [column] is less than or equal to [value]
57+
MockSupabaseQueryBuilder lte(String column, dynamic value) {
58+
_filters[column] = 'lte.$value';
59+
return this;
60+
}
61+
62+
/// Limits the number of rows returned
63+
MockSupabaseQueryBuilder limit(int limit) {
64+
_limitValue = limit;
65+
return this;
66+
}
67+
68+
/// Sets the number of rows to skip
69+
MockSupabaseQueryBuilder offset(int offset) {
70+
_offsetValue = offset;
71+
return this;
72+
}
73+
74+
/// Orders the results by [column] in ascending or descending order
75+
/// Can be called multiple times to sort by multiple columns
76+
MockSupabaseQueryBuilder order(String column, {bool ascending = false}) {
77+
_orderClauses.add((column: column, ascending: ascending));
78+
return this;
79+
}
80+
81+
/// Inserts a new row or rows into the table
82+
List<Map<String, dynamic>> insert(dynamic data) {
83+
if (!_database.containsKey(_table)) {
84+
_database[_table] = [];
85+
}
86+
87+
final List<Map<String, dynamic>> items = data is List
88+
? List<Map<String, dynamic>>.from(data)
89+
: [Map<String, dynamic>.from(data)];
90+
91+
_database[_table]!.addAll(items);
92+
return items;
93+
}
94+
95+
/// Updates rows that match the query filters
96+
List<Map<String, dynamic>> update(Map<String, dynamic> data) {
97+
if (!_database.containsKey(_table)) return [];
98+
99+
final updatedRows = <Map<String, dynamic>>[];
100+
for (var row in _database[_table]!) {
101+
if (_matchesFilters(row)) {
102+
final updatedRow = Map<String, dynamic>.from(row);
103+
updatedRow.addAll(data);
104+
updatedRows.add(updatedRow);
105+
_database[_table]![_database[_table]!.indexOf(row)] = updatedRow;
106+
}
107+
}
108+
return updatedRows;
109+
}
110+
111+
/// Deletes rows that match the query filters
112+
List<Map<String, dynamic>> delete() {
113+
if (!_database.containsKey(_table)) return [];
114+
115+
final deletedRows = <Map<String, dynamic>>[];
116+
_database[_table]!.removeWhere((row) {
117+
if (_matchesFilters(row)) {
118+
deletedRows.add(row);
119+
return true;
120+
}
121+
return false;
122+
});
123+
124+
return deletedRows;
125+
}
126+
127+
/// Selects rows that match the query filters
128+
List<Map<String, dynamic>> select() {
129+
if (!_database.containsKey(_table)) return [];
130+
131+
var result =
132+
_database[_table]!.where((row) => _matchesFilters(row)).toList();
133+
134+
if (_orderClauses.isNotEmpty) {
135+
result.sort((a, b) {
136+
for (final orderClause in _orderClauses) {
137+
final comparison = orderClause.ascending
138+
? a[orderClause.column].compareTo(b[orderClause.column])
139+
: b[orderClause.column].compareTo(a[orderClause.column]);
140+
if (comparison != 0) return comparison;
141+
}
142+
return 0;
143+
});
144+
}
145+
146+
if (_offsetValue != null) {
147+
result = result.skip(_offsetValue!).toList();
148+
}
149+
150+
if (_limitValue != null) {
151+
result = result.take(_limitValue!).toList();
152+
}
153+
154+
return result;
155+
}
156+
157+
bool _matchesFilters(Map<String, dynamic> row) {
158+
for (var entry in _filters.entries) {
159+
final value = entry.value;
160+
if (value.startsWith('eq.')) {
161+
if (row[entry.key].toString() != value.substring(3)) return false;
162+
} else if (value.startsWith('neq.')) {
163+
if (row[entry.key].toString() == value.substring(4)) return false;
164+
} else if (value.startsWith('gt.')) {
165+
if (row[entry.key] <= num.parse(value.substring(3))) return false;
166+
} else if (value.startsWith('lt.')) {
167+
if (row[entry.key] >= num.parse(value.substring(3))) return false;
168+
} else if (value.startsWith('gte.')) {
169+
if (row[entry.key] < num.parse(value.substring(4))) return false;
170+
} else if (value.startsWith('lte.')) {
171+
if (row[entry.key] > num.parse(value.substring(4))) return false;
172+
}
173+
}
174+
return true;
175+
}
176+
}

lib/src/mock_supabase_http_client.dart

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,96 @@ import 'dart:convert';
22

33
import 'package:http/http.dart';
44

5+
import 'mock_supabase_database.dart';
6+
57
class MockSupabaseHttpClient extends BaseClient {
68
final Map<String, List<Map<String, dynamic>>> _database = {};
9+
final Map<
10+
String,
11+
dynamic Function(
12+
MockSupabaseDatabase database, Map<String, dynamic>? params)>
13+
_rpcFunctions = {};
714

815
MockSupabaseHttpClient();
916

1017
void reset() {
11-
// Clear the mock database
18+
// Clear the mock database and RPC functions
1219
_database.clear();
20+
_rpcFunctions.clear();
21+
}
22+
23+
void registerRpcFunction(
24+
String name,
25+
dynamic Function(
26+
MockSupabaseDatabase database, Map<String, dynamic>? params)
27+
function) {
28+
_rpcFunctions[name] = function;
1329
}
1430

1531
@override
1632
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-
);
33+
// Decode the request body if it's not a GET, DELETE, or HEAD request
34+
dynamic body;
35+
if (request.method != 'GET' &&
36+
request.method != 'DELETE' &&
37+
request.method != 'HEAD' &&
38+
request is Request) {
39+
final String requestBody =
40+
await request.finalize().transform(utf8.decoder).join();
41+
if (requestBody.isNotEmpty) {
42+
body = jsonDecode(requestBody);
43+
}
44+
}
45+
46+
// Extract the table name or RPC function name from the URL
47+
final pathSegments = request.url.pathSegments;
48+
final restIndex = pathSegments.indexOf('v1');
49+
if (restIndex != -1 && restIndex < pathSegments.length - 1) {
50+
final resourceName = pathSegments[restIndex + 1];
51+
52+
if (resourceName == 'rpc') {
53+
// Handle RPC call
54+
if (pathSegments.length > restIndex + 2) {
55+
final functionName = pathSegments[restIndex + 2];
56+
return _handleRpc(functionName, request, body);
57+
} else {
58+
return _createResponse({'error': 'RPC function name not provided'},
59+
statusCode: 400, request: request);
60+
}
61+
} else {
62+
// Handle regular database operations
63+
final tableName = _extractTableName(
64+
url: request.url,
65+
headers: request.headers,
66+
method: request.method,
67+
);
2368

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);
69+
// Handle different HTTP methods
70+
switch (request.method) {
71+
case 'POST':
72+
// Handle upsert if the Prefer header is set
73+
final preferHeader = request.headers['Prefer'];
74+
if (preferHeader != null &&
75+
preferHeader.contains('resolution=merge-duplicates')) {
76+
return _handleUpsert(tableName, body, request);
77+
}
78+
return _handleInsert(tableName, body, request);
79+
case 'PATCH':
80+
return _handleUpdate(tableName, body, request);
81+
case 'DELETE':
82+
return _handleDelete(tableName, body, request);
83+
case 'GET':
84+
return _handleSelect(
85+
tableName, request.url.queryParameters, request);
86+
case 'HEAD':
87+
return _handleHead(tableName, request.url.queryParameters, request);
88+
default:
89+
return _createResponse({'error': 'Method not allowed'},
90+
statusCode: 405, request: request);
4091
}
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);
92+
}
5393
}
94+
throw Exception('Invalid URL format: unable to extract table name');
5495
}
5596

5697
String _extractTableName({
@@ -706,4 +747,23 @@ class MockSupabaseHttpClient extends BaseClient {
706747
request: request,
707748
);
708749
}
750+
751+
StreamedResponse _handleRpc(
752+
String functionName, BaseRequest request, dynamic body) {
753+
if (!_rpcFunctions.containsKey(functionName)) {
754+
return _createResponse({'error': 'RPC function not found'},
755+
statusCode: 404, request: request);
756+
}
757+
758+
final function = _rpcFunctions[functionName]!;
759+
760+
try {
761+
final mockDatabase = MockSupabaseDatabase(_database);
762+
final result = function(mockDatabase, body);
763+
return _createResponse(result, request: request);
764+
} catch (e) {
765+
return _createResponse({'error': 'RPC function execution failed: $e'},
766+
statusCode: 500, request: request);
767+
}
768+
}
709769
}

0 commit comments

Comments
 (0)