Skip to content

chore: Code cleanup #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions lib/mock_supabase_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@
library;

export 'src/mock_supabase_http_client.dart';

// TODO: Export any libraries intended for clients of this package.
84 changes: 84 additions & 0 deletions lib/src/handlers/rpc_handler.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic>? params,
Map<String, List<Map<String, dynamic>>> tables)> _rpcFunctions;
final Map<String, List<Map<String, dynamic>>> _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<String, String>? 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<String, dynamic>? params,
Map<String, List<Map<String, dynamic>>> tables)
function,
) {
_rpcFunctions[name] = function;
}

/// Clears all registered RPC functions
void reset() {
_rpcFunctions.clear();
}
}
227 changes: 14 additions & 213 deletions lib/src/mock_supabase_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@ import 'dart:convert';

import 'package:http/http.dart';

import 'handlers/rpc_handler.dart';
import 'utils/filter_parser.dart';

class MockSupabaseHttpClient extends BaseClient {
final Map<String, List<Map<String, dynamic>>> _database = {};
final Map<
String,
dynamic Function(Map<String, dynamic>? params,
Map<String, List<Map<String, dynamic>>> 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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String, dynamic> row,
required Map<String, String> 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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 : {},
Expand Down Expand Up @@ -623,183 +619,6 @@ class MockSupabaseHttpClient extends BaseClient {
);
}

bool Function(Map<String, dynamic> row) _parseFilter({
required String columnName,
required String postrestFilter,
required Map<String, dynamic> 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<String, dynamic>;
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<String, dynamic> 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,
Expand All @@ -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);
}
}
}
Loading