From 8f7c9481661459fe40354adb22c3fad94b007e3f Mon Sep 17 00:00:00 2001 From: dshukertjr Date: Fri, 25 Oct 2024 11:35:14 +0900 Subject: [PATCH] chore: Code cleanup --- lib/mock_supabase_http_client.dart | 2 - lib/src/handlers/rpc_handler.dart | 84 +++++++++ lib/src/mock_supabase_http_client.dart | 227 ++----------------------- lib/src/utils/filter_parser.dart | 185 ++++++++++++++++++++ 4 files changed, 283 insertions(+), 215 deletions(-) create mode 100644 lib/src/handlers/rpc_handler.dart create mode 100644 lib/src/utils/filter_parser.dart diff --git a/lib/mock_supabase_http_client.dart b/lib/mock_supabase_http_client.dart index 22b1185..00a1f0b 100644 --- a/lib/mock_supabase_http_client.dart +++ b/lib/mock_supabase_http_client.dart @@ -4,5 +4,3 @@ library; export 'src/mock_supabase_http_client.dart'; - -// TODO: Export any libraries intended for clients of this package. diff --git a/lib/src/handlers/rpc_handler.dart b/lib/src/handlers/rpc_handler.dart new file mode 100644 index 0000000..f1f5bd0 --- /dev/null +++ b/lib/src/handlers/rpc_handler.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +/// Handles RPC (Remote Procedure Call) operations for the mock Supabase client +class RpcHandler { + final Map< + String, + dynamic Function(Map? params, + Map>> tables)> _rpcFunctions; + final Map>> _database; + + RpcHandler(this._rpcFunctions, this._database); + + /// Handles an RPC call + /// + /// [functionName] The name of the RPC function to call + /// [request] The original HTTP request + /// [body] The parsed request body containing parameters + 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, + ); + } + } + + /// Creates a StreamedResponse with the given data and headers + StreamedResponse _createResponse( + dynamic data, { + int statusCode = 200, + required BaseRequest request, + Map? headers, + }) { + final responseHeaders = { + 'content-type': 'application/json; charset=utf-8', + ...?headers, + }; + + return StreamedResponse( + Stream.value(utf8.encode(jsonEncode(data))), + statusCode, + headers: responseHeaders, + request: request, + ); + } + + /// Registers a new RPC function + /// + /// [name] The name of the function to register + /// [function] The function implementation + void registerFunction( + String name, + dynamic Function(Map? params, + Map>> tables) + function, + ) { + _rpcFunctions[name] = function; + } + + /// Clears all registered RPC functions + void reset() { + _rpcFunctions.clear(); + } +} diff --git a/lib/src/mock_supabase_http_client.dart b/lib/src/mock_supabase_http_client.dart index cf96ef3..999b756 100644 --- a/lib/src/mock_supabase_http_client.dart +++ b/lib/src/mock_supabase_http_client.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'package:http/http.dart'; +import 'handlers/rpc_handler.dart'; +import 'utils/filter_parser.dart'; + class MockSupabaseHttpClient extends BaseClient { final Map>> _database = {}; final Map< @@ -9,12 +12,16 @@ class MockSupabaseHttpClient extends BaseClient { dynamic Function(Map? params, Map>> tables)> _rpcFunctions = {}; - MockSupabaseHttpClient(); + late final RpcHandler _rpcHandler; + + MockSupabaseHttpClient() { + _rpcHandler = RpcHandler(_rpcFunctions, _database); + } void reset() { // Clear the mock database and RPC functions _database.clear(); - _rpcFunctions.clear(); + _rpcHandler.reset(); } /// Registers a RPC function that can be called using the `rpc` method on a `Postgrest` client. @@ -122,7 +129,7 @@ class MockSupabaseHttpClient extends BaseClient { // Handle RPC call if (pathSegments.length > restIndex + 2) { final functionName = pathSegments[restIndex + 2]; - return _handleRpc(functionName, request, body); + return _rpcHandler.handleRpc(functionName, request, body); } else { return _createResponse({'error': 'RPC function name not provided'}, statusCode: 400, request: request); @@ -250,23 +257,12 @@ class MockSupabaseHttpClient extends BaseClient { } } - /// Checks if a given item matches the provided filters. - /// - /// This method iterates through each filter in the `filters` map, - /// parses the filter using `_parseFilter`, and applies it to the `item`. - /// If any filter doesn't match, the method returns false. - /// If all filters match, it returns true. - /// - /// [row] The item to check against the filters. - /// [filters] A map of filter keys and their corresponding values. - /// Returns true if the item matches all filters, false otherwise. bool _matchesFilters({ required Map row, required Map filters, }) { - // Check if an item matches the provided filters for (var columnName in filters.keys) { - final filter = _parseFilter( + final filter = FilterParser.parseFilter( columnName: columnName, postrestFilter: filters[columnName]!, targetRow: row, @@ -355,7 +351,7 @@ class MockSupabaseHttpClient extends BaseClient { final parts = key.split('.'); final referencedTableName = parts[0]; final referencedColumnName = parts[1]; - final filter = _parseFilter( + final filter = FilterParser.parseFilter( columnName: referencedColumnName, postrestFilter: value, targetRow: returningRows.first[referencedTableName] is List @@ -390,7 +386,7 @@ class MockSupabaseHttpClient extends BaseClient { // referenced table filtering with !inner } else { // Regular filtering on the top level table - final filter = _parseFilter( + final filter = FilterParser.parseFilter( columnName: key, postrestFilter: value, targetRow: returningRows.first, @@ -580,7 +576,7 @@ class MockSupabaseHttpClient extends BaseClient { key != 'order' && key != 'limit' && key != 'range') { - final filter = _parseFilter( + final filter = FilterParser.parseFilter( columnName: key, postrestFilter: value, targetRow: returningRows.isNotEmpty ? returningRows.first : {}, @@ -623,183 +619,6 @@ class MockSupabaseHttpClient extends BaseClient { ); } - bool Function(Map row) _parseFilter({ - required String columnName, - required String postrestFilter, - required Map targetRow, - }) { - // Parse filters from query parameters - if (columnName == 'or') { - final orFilters = - postrestFilter.substring(1, postrestFilter.length - 1).split(','); - return (row) { - return orFilters.any((filter) { - final parts = filter.split('.'); - final subColumnName = parts[0]; - final operator = parts[1]; - final value = parts.sublist(2).join('.'); - final subFilter = _parseFilter( - columnName: subColumnName, - postrestFilter: '$operator.$value', - targetRow: row); - return subFilter(row); - }); - }; - } else if (postrestFilter.startsWith('eq.')) { - final value = postrestFilter.substring(3); - return (row) => row[columnName].toString() == value; - } else if (postrestFilter.startsWith('neq.')) { - final value = postrestFilter.substring(4); - return (row) => row[columnName].toString() != value; - } else if (postrestFilter.startsWith('gt.')) { - return _handleComparison( - operator: 'gt', - value: postrestFilter.substring(3), - columnName: columnName, - ); - } else if (postrestFilter.startsWith('lt.')) { - return _handleComparison( - operator: 'lt', - value: postrestFilter.substring(3), - columnName: columnName, - ); - } else if (postrestFilter.startsWith('gte.')) { - return _handleComparison( - operator: 'gte', - value: postrestFilter.substring(4), - columnName: columnName, - ); - } else if (postrestFilter.startsWith('lte.')) { - return _handleComparison( - operator: 'lte', - value: postrestFilter.substring(4), - columnName: columnName, - ); - } else if (postrestFilter.startsWith('like.')) { - final value = postrestFilter.substring(5); - final regex = RegExp(value.replaceAll('%', '.*')); - return (row) => regex.hasMatch(row[columnName]); - } else if (postrestFilter == 'is.null') { - return (row) => row[columnName] == null; - } else if (postrestFilter.startsWith('in.')) { - final value = postrestFilter.substring(3); - final values = value.substring(1, value.length - 1).split(','); - return (row) => values.contains(row[columnName].toString()); - } else if (postrestFilter.startsWith('cs.')) { - final value = postrestFilter.substring(3); - if (value.startsWith('{') && value.endsWith('}')) { - // Array case - final values = value.substring(1, value.length - 1).split(','); - return (row) => values.every((v) { - final decodedValue = v.startsWith('"') && v.endsWith('"') - ? jsonDecode(v) - : v.toString(); - return (row[columnName] as List).contains(decodedValue); - }); - } else { - throw UnimplementedError( - 'JSON and range operators in contains is not yet supported'); - } - } else if (postrestFilter.startsWith('containedBy.')) { - final value = postrestFilter.substring(12); - final values = jsonDecode(value); - return (row) => - values.every((v) => (row[columnName] as List).contains(v)); - } else if (postrestFilter.startsWith('overlaps.')) { - final value = postrestFilter.substring(9); - final values = jsonDecode(value); - return (row) => - (row[columnName] as List).any((element) => values.contains(element)); - } else if (postrestFilter.startsWith('fts.')) { - final value = postrestFilter.substring(4); - return (row) => (row[columnName] as String).contains(value); - } else if (postrestFilter.startsWith('match.')) { - final value = jsonDecode(postrestFilter.substring(6)); - return (row) { - if (row[columnName] is! Map) return false; - final rowMap = row[columnName] as Map; - return value.entries.every((entry) => rowMap[entry.key] == entry.value); - }; - } else if (postrestFilter.startsWith('not.')) { - final parts = postrestFilter.split('.'); - final operator = parts[1]; - final value = parts.sublist(2).join('.'); - final filter = _parseFilter( - columnName: columnName, - postrestFilter: '$operator.$value', - targetRow: targetRow, - ); - return (row) => !filter(row); - } - return (row) => true; - } - - /// Handles comparison operations for date and numeric values. - /// - /// This function creates a filter based on the given comparison [operator], - /// [value], and [columnName]. It supports both date and numeric comparisons. - /// - /// [operator] can be 'gt', 'lt', 'gte', or 'lte'. - /// [value] is the string representation of the value to compare against. - /// [columnName] is the name of the column to compare in each row. - /// - /// Returns a function that takes a row and returns a boolean indicating - /// whether the row matches the comparison criteria. - bool Function(Map row) _handleComparison({ - required String operator, - required String value, - required String columnName, - }) { - // Check if the value is a valid date - if (DateTime.tryParse(value) != null) { - final dateTime = DateTime.parse(value); - return (row) { - final rowDate = DateTime.tryParse(row[columnName].toString()); - if (rowDate == null) return false; - // Perform date comparison based on the operator - switch (operator) { - case 'gt': - return rowDate.isAfter(dateTime); - case 'lt': - return rowDate.isBefore(dateTime); - case 'gte': - return rowDate.isAtSameMomentAs(dateTime) || - rowDate.isAfter(dateTime); - case 'lte': - return rowDate.isAtSameMomentAs(dateTime) || - rowDate.isBefore(dateTime); - default: - throw UnimplementedError('Unsupported operator: $operator'); - } - }; - } - // Check if the value is a valid number - else if (num.tryParse(value) != null) { - final numValue = num.parse(value); - return (row) { - final rowValue = num.tryParse(row[columnName].toString()); - if (rowValue == null) return false; - // Perform numeric comparison based on the operator - switch (operator) { - case 'gt': - return rowValue > numValue; - case 'lt': - return rowValue < numValue; - case 'gte': - return rowValue >= numValue; - case 'lte': - return rowValue <= numValue; - default: - throw UnimplementedError('Unsupported operator: $operator'); - } - }; - } - // Throw an error if the value is neither a date nor a number - else { - throw UnimplementedError('Unsupported value type'); - } - } - StreamedResponse _createResponse(dynamic data, {int statusCode = 200, required BaseRequest request, @@ -816,22 +635,4 @@ 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); - } - } } diff --git a/lib/src/utils/filter_parser.dart b/lib/src/utils/filter_parser.dart new file mode 100644 index 0000000..bb4be62 --- /dev/null +++ b/lib/src/utils/filter_parser.dart @@ -0,0 +1,185 @@ +import 'dart:convert'; + +/// A utility class for parsing and handling Postgrest filters +class FilterParser { + /// Parses a filter string and returns a function that can be used to filter rows + /// based on the parsed conditions. + /// + /// [columnName] The name of the column to filter on + /// [postrestFilter] The filter string in Postgrest format + /// [targetRow] A sample row used to determine data types + static bool Function(Map row) parseFilter({ + required String columnName, + required String postrestFilter, + required Map targetRow, + }) { + // Parse filters from query parameters + if (columnName == 'or') { + final orFilters = + postrestFilter.substring(1, postrestFilter.length - 1).split(','); + return (row) { + return orFilters.any((filter) { + final parts = filter.split('.'); + final subColumnName = parts[0]; + final operator = parts[1]; + final value = parts.sublist(2).join('.'); + final subFilter = parseFilter( + columnName: subColumnName, + postrestFilter: '$operator.$value', + targetRow: row); + return subFilter(row); + }); + }; + } else if (postrestFilter.startsWith('eq.')) { + final value = postrestFilter.substring(3); + return (row) => row[columnName].toString() == value; + } else if (postrestFilter.startsWith('neq.')) { + final value = postrestFilter.substring(4); + return (row) => row[columnName].toString() != value; + } else if (postrestFilter.startsWith('gt.')) { + return _handleComparison( + operator: 'gt', + value: postrestFilter.substring(3), + columnName: columnName, + ); + } else if (postrestFilter.startsWith('lt.')) { + return _handleComparison( + operator: 'lt', + value: postrestFilter.substring(3), + columnName: columnName, + ); + } else if (postrestFilter.startsWith('gte.')) { + return _handleComparison( + operator: 'gte', + value: postrestFilter.substring(4), + columnName: columnName, + ); + } else if (postrestFilter.startsWith('lte.')) { + return _handleComparison( + operator: 'lte', + value: postrestFilter.substring(4), + columnName: columnName, + ); + } else if (postrestFilter.startsWith('like.')) { + final value = postrestFilter.substring(5); + final regex = RegExp(value.replaceAll('%', '.*')); + return (row) => regex.hasMatch(row[columnName].toString()); + } else if (postrestFilter == 'is.null') { + return (row) => row[columnName] == null; + } else if (postrestFilter.startsWith('in.')) { + final value = postrestFilter.substring(3); + final values = value.substring(1, value.length - 1).split(','); + return (row) => values.contains(row[columnName].toString()); + } else if (postrestFilter.startsWith('cs.')) { + final value = postrestFilter.substring(3); + if (value.startsWith('{') && value.endsWith('}')) { + // Array case + final values = value.substring(1, value.length - 1).split(','); + return (row) => values.every((v) { + final decodedValue = v.startsWith('"') && v.endsWith('"') + ? jsonDecode(v) + : v.toString(); + return (row[columnName] as List).contains(decodedValue); + }); + } else { + throw UnimplementedError( + 'JSON and range operators in contains is not yet supported'); + } + } else if (postrestFilter.startsWith('containedBy.')) { + final value = postrestFilter.substring(12); + final values = jsonDecode(value); + return (row) => + values.every((v) => (row[columnName] as List).contains(v)); + } else if (postrestFilter.startsWith('overlaps.')) { + final value = postrestFilter.substring(9); + final values = jsonDecode(value); + return (row) => + (row[columnName] as List).any((element) => values.contains(element)); + } else if (postrestFilter.startsWith('fts.')) { + final value = postrestFilter.substring(4); + return (row) => (row[columnName] as String).contains(value); + } else if (postrestFilter.startsWith('match.')) { + final value = jsonDecode(postrestFilter.substring(6)); + return (row) { + if (row[columnName] is! Map) return false; + final rowMap = row[columnName] as Map; + return value.entries.every((entry) => rowMap[entry.key] == entry.value); + }; + } else if (postrestFilter.startsWith('not.')) { + final parts = postrestFilter.split('.'); + final operator = parts[1]; + final value = parts.sublist(2).join('.'); + final filter = parseFilter( + columnName: columnName, + postrestFilter: '$operator.$value', + targetRow: targetRow, + ); + return (row) => !filter(row); + } + return (row) => true; + } + + /// Handles comparison operations for date and numeric values. + /// + /// This function creates a filter based on the given comparison [operator], + /// [value], and [columnName]. It supports both date and numeric comparisons. + /// + /// [operator] can be 'gt', 'lt', 'gte', or 'lte'. + /// [value] is the string representation of the value to compare against. + /// [columnName] is the name of the column to compare in each row. + /// + /// Returns a function that takes a row and returns a boolean indicating + /// whether the row matches the comparison criteria. + static bool Function(Map row) _handleComparison({ + required String operator, + required String value, + required String columnName, + }) { + // Check if the value is a valid date + if (DateTime.tryParse(value) != null) { + final dateTime = DateTime.parse(value); + return (row) { + final rowDate = DateTime.tryParse(row[columnName].toString()); + if (rowDate == null) return false; + switch (operator) { + case 'gt': + return rowDate.isAfter(dateTime); + case 'lt': + return rowDate.isBefore(dateTime); + case 'gte': + return rowDate.isAtSameMomentAs(dateTime) || + rowDate.isAfter(dateTime); + case 'lte': + return rowDate.isAtSameMomentAs(dateTime) || + rowDate.isBefore(dateTime); + default: + throw UnimplementedError('Unsupported operator: $operator'); + } + }; + } + // Check if the value is a valid number + else if (num.tryParse(value) != null) { + final numValue = num.parse(value); + return (row) { + final rowValue = num.tryParse(row[columnName].toString()); + if (rowValue == null) return false; + switch (operator) { + case 'gt': + return rowValue > numValue; + case 'lt': + return rowValue < numValue; + case 'gte': + return rowValue >= numValue; + case 'lte': + return rowValue <= numValue; + default: + throw UnimplementedError('Unsupported operator: $operator'); + } + }; + } + // Throw an error if the value is neither a date nor a number + else { + throw UnimplementedError('Unsupported value type'); + } + } +}