Skip to content
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
1 change: 1 addition & 0 deletions lib/http/request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export '../src/http/validation/custom_validation_rule.dart';
export '../src/exception/base_http_exception.dart';
export '../src/exception/http_exception.dart';
export '../src/exception/redirect_exception.dart';
export '../src/utils/request_helper.dart';
8 changes: 8 additions & 0 deletions lib/src/contract/http/request/form_validation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import '../../../http/validation/custom_validation_rule.dart';

abstract class FormValidation {
dynamic rules();
List<CustomValidationRule> customRule() => [];
Map<String, String> messages() => {};
bool authorize() => true;
}
20 changes: 18 additions & 2 deletions lib/src/database/_connection_manager.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:vania/src/exception/database_exception.dart';

import '../contract/database/_connectors/_database_connection.dart';
Expand Down Expand Up @@ -25,8 +27,10 @@ class ConnectionManager {
bool get isConnected => connectionMap.isNotEmpty;

DatabaseConnection? connection([String? connectionName]) {
final name = connectionName ?? defaultConnection;
return connectionMap[name];
if (connectionName == null || connectionName.isEmpty) {
connectionName = defaultConnection;
}
return connectionMap[connectionName];
}

Future<void> connect(DBConfig config, String connectionName) async {
Expand All @@ -40,6 +44,10 @@ class ConnectionManager {
_monitor,
);
connectionMap[connectionName] = monitoredConnection;

if (!config.pool) {
await _checkDatabaseHealth(monitoredConnection);
}
} on InvalidArgumentException catch (e) {
Logger.log(e.message, type: Logger.ERROR);
throw DatabaseException(
Expand All @@ -49,6 +57,14 @@ class ConnectionManager {
}
}

Future<void> _checkDatabaseHealth(DatabaseConnection connection) async {
Timer.periodic(Duration(minutes: 5), (timer) async {
try {
await connection.execute('SELECT 1;');
} catch (_) {}
});
}

Future<bool> transaction(
Future<bool> Function() action, [
String? connectionName,
Expand Down
5 changes: 3 additions & 2 deletions lib/src/database/orm/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:vania/src/exception/invalid_argument_exception.dart';
import 'package:vania/src/utils/_pluralize.dart';
import 'package:vania/src/utils/functions.dart';

import '../../utils/request_helper.dart' show getParam;
import 'belongs_to.dart';
import 'belongs_to_many.dart';
import 'has_many.dart';
Expand Down Expand Up @@ -549,7 +550,7 @@ abstract class Model extends QueryBuilderImpl {
String? pageName,
int? page,
}) async {
int currentPage = page ?? 1;
int currentPage = page ?? getParam<int>('page', 1)!;
int total = await count();
final lastPage = (total / perPage).ceil();
final offset = (currentPage - 1) * perPage;
Expand Down Expand Up @@ -590,7 +591,7 @@ abstract class Model extends QueryBuilderImpl {
String? pageName,
int? page,
]) async {
int currentPage = page ?? 1;
int currentPage = page ?? getParam<int>('page', 1)!;
int total = await count();
final lastPage = (total / perPage).ceil();
final offset = (currentPage - 1) * perPage;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:vania/src/utils/request_helper.dart' show getParam;

import '../../contract/database/_connectors/_database_connection.dart';
import '../../exception/invalid_argument_exception.dart';

Expand Down Expand Up @@ -327,7 +329,7 @@ abstract mixin class QueryExecutorBuilderImpl implements QueryBuilder {
String? pageName,
int? page,
}) async {
int currentPage = page ?? 1;
int currentPage = page ?? getParam<int>('page', 1)!;
int total = await count();
final lastPage = (total / perPage).ceil();
final offset = (currentPage - 1) * perPage;
Expand Down Expand Up @@ -369,7 +371,7 @@ abstract mixin class QueryExecutorBuilderImpl implements QueryBuilder {
String? pageName,
int? page,
]) async {
int currentPage = page ?? 1;
int currentPage = page ?? getParam<int>('page', 1)!;
int total = await count();
final lastPage = (total / perPage).ceil();
final offset = (currentPage - 1) * perPage;
Expand Down
5 changes: 5 additions & 0 deletions lib/src/exception/unauthorized_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'base_http_exception.dart';

class UnauthorizedException extends BaseHttpResponseException {
UnauthorizedException({required super.message, required super.code});
}
11 changes: 8 additions & 3 deletions lib/src/extensions/map_extension.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extension MapExtensions on Map<dynamic, dynamic> {
extension MapExtensions on Map<String, dynamic> {
/// Removes a parameter from a nested map structure based on a dot-separated key path.
///
/// This method allows removing a value from a deeply nested map using a path described by a string of keys separated by dots.
Expand Down Expand Up @@ -65,14 +65,19 @@ extension MapExtensions on Map<dynamic, dynamic> {
/// ```
dynamic getParam(String keys) {
List<String> parts = keys.split('.');
Map<dynamic, dynamic> data = this;
Map<String, dynamic> data = this;
for (int i = 0; i < parts.length - 1; i++) {
if (data[parts[i]] is Map) {
data = data[parts[i]];
} else {
return null; // Return null if the path breaks
return [];
}
}
if (data[parts.last] is List) {
List<Map<String, dynamic>> list =
List.castFrom<dynamic, Map<String, dynamic>>(data[parts.last]);
return list;
}
return data[parts.last];
}
}
3 changes: 3 additions & 0 deletions lib/src/http/controller/controller_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:vania/src/http/response/response.dart';
import 'package:vania/src/route/route_data.dart';
import 'package:vania/src/route/route_history.dart';

import '../../exception/base_http_exception.dart';
import '../../exception/invalid_argument_exception.dart';

class ControllerHandler {
Expand Down Expand Up @@ -67,6 +68,8 @@ class ControllerHandler {
_response(request, error.message, 500);
} on QueryException catch (error) {
_response(request, error.cause ?? '', 500);
} on BaseHttpResponseException catch (error) {
_response(request, error.message, error.code);
} catch (error) {
_response(request, error.toString());
}
Expand Down
72 changes: 66 additions & 6 deletions lib/src/http/request/request.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'package:vania/src/authentication/authentication.dart';
import 'package:vania/src/contract/http/request/form_validation.dart';
import 'package:vania/src/exception/validation_exception.dart';
import 'package:vania/src/http/request/request_body.dart';
import 'package:vania/src/http/request/request_file.dart';
Expand All @@ -12,11 +13,19 @@ import 'package:vania/src/http/validation/validator.dart';
import 'package:vania/src/route/route_data.dart';
import 'package:vania/src/view_engine/template_engine.dart';

import '../../exception/unauthorized_exception.dart';
import '../validation/field_validation.dart';

class Request {
final HttpRequest request;
final RouteData? route;
late HttpRequest request;
RouteData? route;

Request from({required HttpRequest request, RouteData? route}) {
this.request = request;
this.route = route;

Request.from({required this.request, this.route});
return this;
}

Map? get user => Auth().user();

Expand Down Expand Up @@ -339,16 +348,67 @@ class Request {
dynamic rules, [
Map<String, String> messages = const <String, String>{},
]) async {
assert(rules is Map<String, String> || rules is List<Validation>,
'Rules must be either Map<String, String> or List<Validation>.');
assert(
rules is Map<String, String> ||
rules is List<FieldValidation> ||
rules is List<Validation> ||
rules is FormValidation,
'Rules must be either Map<String, String> or List<Validation>. or FormRequest');
TemplateEngine().sessionErrors.clear();
if (rules is Map<String, String>) {
await _validate(rules, messages);
} else if (rules is List<FieldValidation>) {
Map<String, String> ruleMessages = Map.from(messages);
final rulesMap = Map.fromEntries(rules.map((rule) {
ruleMessages.addAll(rule.toMapMessages);
return MapEntry(rule.fieldName, rule.toString());
}));
await _validate(rulesMap, ruleMessages);
} else if (rules is FormValidation) {
await _formRequestValidate(rules);
} else {
_validateChain(rules as List<Validation>);
}
}

Future<void> _formRequestValidate(FormValidation formRequest) async {
if (!formRequest.authorize()) {
throw UnauthorizedException(message: 'Access denied', code: 403);
}

final rules = formRequest.rules();
final Map<String, String> messages = formRequest.messages();

Validator validator = Validator(data: body);

if (formRequest.customRule().isNotEmpty) {
validator.customRule(formRequest.customRule());
}

Map<String, String> rulesMap = {};
if (rules is List<FieldValidation>) {
rulesMap = Map.fromEntries(rules.map((rule) {
messages.addAll(rule.toMapMessages);
return MapEntry(rule.fieldName, rule.toString());
}));
} else {
rulesMap = formRequest.rules();
}

if (messages.isNotEmpty) {
validator.setNewMessages(messages);
}

await validator.validate(rulesMap);
if (validator.hasError) {
bool isHtml = request.headers.value('accept').toString().contains('html');
if (isHtml) {
TemplateEngine().sessionErrors.addAll(validator.errors);
}
throw ValidationException(message: validator.errors);
}
}

Future<void> _validate(
Map<String, String> rules, [
Map<String, String> messages = const <String, String>{},
Expand Down Expand Up @@ -381,7 +441,7 @@ class Request {
for (ValidationRule rule in validation.rules) {
if (!rule.validate(fieldValue, data)) {
errors[validation.field] = rule.errorMessage;
break; // Stop at the first failed validation per field
break;
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/src/http/request/request_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import 'package:vania/src/logger/logger.dart';
import 'package:vania/src/utils/helper.dart';
import 'request.dart';

HttpRequest? globalHttpRequest;

class RequestHandler {
/// Handles HTTP requests, determining if the request is a WebSocket upgrade or
/// a standard HTTP request. If it's a WebSocket request, it delegates handling
Expand All @@ -38,6 +40,8 @@ class RequestHandler {
/// - [BaseHttpResponseException] if there is an issue with the HTTP response.
/// - [InvalidArgumentException] if an invalid argument is encountered.
Future handle(HttpRequest req) async {
globalHttpRequest = req;

/// Check the incoming request is web socket or not
if (env<bool>('APP_WEBSOCKET', false) &&
WebSocketTransformer.isUpgradeRequest(req)) {
Expand All @@ -53,7 +57,7 @@ class RequestHandler {
String requestMethod = req.method.toUpperCase();

if (route != null) {
Request request = Request.from(request: req, route: route);
Request request = Request().from(request: req, route: route);
await request.extractBody();

if (isHtml) {
Expand Down
Loading
Loading