Repository: vania-dart/framework Branch: dev Commit: a8a5f382a506 Files: 280 Total size: 599.6 KB Directory structure: gitextract_qi8hzr2l/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── documentation-issue.md │ │ └── feature_request.md │ ├── stale.yml │ └── workflows/ │ └── vania-dart.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README.tr.md ├── analysis_options.yaml ├── example/ │ ├── app_exception_handler.dart │ └── main.dart ├── lib/ │ ├── application.dart │ ├── authentication.dart │ ├── database.dart │ ├── http/ │ │ ├── controller.dart │ │ ├── form_validation.dart │ │ ├── middleware.dart │ │ ├── request.dart │ │ └── response.dart │ ├── mail.dart │ ├── migration.dart │ ├── orm/ │ │ └── model.dart │ ├── query_builder.dart │ ├── route.dart │ ├── service_provider.dart │ ├── src/ │ │ ├── authentication/ │ │ │ ├── authenticate.dart │ │ │ ├── authentication.dart │ │ │ ├── gate/ │ │ │ │ └── gate.dart │ │ │ ├── has_api_tokens.dart │ │ │ ├── model/ │ │ │ │ └── personal_access_token.dart │ │ │ └── redirect_if_authenticated.dart │ │ ├── aws/ │ │ │ └── s3_client.dart │ │ ├── cache/ │ │ │ ├── cache.dart │ │ │ ├── cache_driver.dart │ │ │ ├── file_cache_driver.dart │ │ │ └── redis_cache_driver.dart │ │ ├── config/ │ │ │ ├── config.dart │ │ │ ├── defined_regexp.dart │ │ │ └── http_cors.dart │ │ ├── container.dart │ │ ├── contract/ │ │ │ ├── database/ │ │ │ │ ├── _connectors/ │ │ │ │ │ └── _database_connection.dart │ │ │ │ └── query_builder/ │ │ │ │ ├── _bulk_operations_builder.dart │ │ │ │ ├── _cte_builder.dart │ │ │ │ ├── _delete_query_builder.dart │ │ │ │ ├── _insert_query_builder.dart │ │ │ │ ├── _join_clause_builder.dart │ │ │ │ ├── _query_executor_builder.dart │ │ │ │ ├── _select_query_builder.dart │ │ │ │ ├── _union_clause_builder.dart │ │ │ │ ├── _update_query_builder.dart │ │ │ │ ├── _where_clauses_builder.dart │ │ │ │ ├── _window_functions_builder.dart │ │ │ │ └── query_builder.dart │ │ │ ├── http/ │ │ │ │ └── request/ │ │ │ │ └── form_validation.dart │ │ │ └── orm/ │ │ │ ├── morph_relation.dart │ │ │ └── relation.dart │ │ ├── cryptographic/ │ │ │ ├── hash.dart │ │ │ └── vania_encryption.dart │ │ ├── database/ │ │ │ ├── _connection_manager.dart │ │ │ ├── _connectors/ │ │ │ │ ├── _database_connection_factory.dart │ │ │ │ └── _database_connection_proxy.dart │ │ │ ├── _database_utils/ │ │ │ │ ├── _db_config.dart │ │ │ │ ├── _paginated_result.dart │ │ │ │ ├── _raw_expression.dart │ │ │ │ └── _singularize.dart │ │ │ ├── adapters/ │ │ │ │ ├── _mysql_connector.dart │ │ │ │ ├── _postgres_connector.dart │ │ │ │ └── _sqlite_connector.dart │ │ │ ├── db.dart │ │ │ ├── isolate_db.dart │ │ │ ├── migration/ │ │ │ │ ├── adapters/ │ │ │ │ │ ├── grammar/ │ │ │ │ │ │ ├── mysql_grammar.dart │ │ │ │ │ │ ├── postgresql_grammar.dart │ │ │ │ │ │ ├── sql_grammar.dart │ │ │ │ │ │ └── sqlite_grammar.dart │ │ │ │ │ ├── mysql_adapter.dart │ │ │ │ │ ├── postgresql_adapter.dart │ │ │ │ │ └── sqlite_adapter.dart │ │ │ │ ├── builders/ │ │ │ │ │ ├── column_definition.dart │ │ │ │ │ ├── column_types.dart │ │ │ │ │ ├── schema.dart │ │ │ │ │ └── table_definition.dart │ │ │ │ ├── contracts/ │ │ │ │ │ ├── database_adapter_interface.dart │ │ │ │ │ ├── migration_connection_interface.dart │ │ │ │ │ └── schema_interface.dart │ │ │ │ ├── migration.dart │ │ │ │ ├── migration_connection.dart │ │ │ │ └── runners/ │ │ │ │ └── migration_runner.dart │ │ │ ├── monitoring/ │ │ │ │ └── database_monitor.dart │ │ │ ├── orm/ │ │ │ │ ├── belongs_to.dart │ │ │ │ ├── belongs_to_many.dart │ │ │ │ ├── has_many.dart │ │ │ │ ├── has_one.dart │ │ │ │ ├── model.dart │ │ │ │ └── polymorphic/ │ │ │ │ ├── morph_many.dart │ │ │ │ ├── morph_one.dart │ │ │ │ ├── morph_to.dart │ │ │ │ ├── morph_to_many.dart │ │ │ │ └── morphed_by_many.dart │ │ │ ├── query_builder/ │ │ │ │ ├── _bulk_operations_builder_impl.dart │ │ │ │ ├── _cte/ │ │ │ │ │ ├── _cte_cache.dart │ │ │ │ │ ├── _cte_configuration.dart │ │ │ │ │ ├── _cte_definition.dart │ │ │ │ │ ├── _cte_exception.dart │ │ │ │ │ ├── _cte_feature.dart │ │ │ │ │ ├── _database_type.dart │ │ │ │ │ ├── _duplicate_cte_name_exception.dart │ │ │ │ │ ├── _invalid_cte_configuration_exception.dart │ │ │ │ │ ├── _sql_identifier_escaper.dart │ │ │ │ │ ├── _standard_escaping_strategy.dart │ │ │ │ │ └── _unsupported_cte_feature_exception.dart │ │ │ │ ├── _cte_builder_impl.dart │ │ │ │ ├── _delete_query_builder_impl.dart │ │ │ │ ├── _insert_query_builder_impl.dart │ │ │ │ ├── _join_clause_builder_impl.dart │ │ │ │ ├── _query_builder_impl.dart │ │ │ │ ├── _query_executor_builder_impl.dart │ │ │ │ ├── _select_query_builder_impl.dart │ │ │ │ ├── _union_clause_builder_impl.dart │ │ │ │ ├── _update_query_builder_impl.dart │ │ │ │ ├── _where_clauses_builder_impl.dart │ │ │ │ └── _window_functions_builder_impl.dart │ │ │ └── seeder/ │ │ │ ├── seeder.dart │ │ │ ├── seeder_factory.dart │ │ │ └── seeder_runner.dart │ │ ├── enum/ │ │ │ ├── column_index.dart │ │ │ └── http_request_method.dart │ │ ├── env_handler/ │ │ │ └── env.dart │ │ ├── exception/ │ │ │ ├── base_http_exception.dart │ │ │ ├── database_exception.dart │ │ │ ├── exception_handler.dart │ │ │ ├── forbidden_exception.dart │ │ │ ├── http_exception.dart │ │ │ ├── internal_server_error.dart │ │ │ ├── invalid_argument_exception.dart │ │ │ ├── not_found_exception.dart │ │ │ ├── page_expired_exception.dart │ │ │ ├── query_exception.dart │ │ │ ├── redirect_exception.dart │ │ │ ├── throttle_exception.dart │ │ │ ├── unauthenticated.dart │ │ │ ├── unauthorized_exception.dart │ │ │ └── validation_exception.dart │ │ ├── extensions/ │ │ │ ├── date_time_extension.dart │ │ │ ├── extensions.dart │ │ │ ├── localization_extension.dart │ │ │ ├── map_extension.dart │ │ │ ├── number_extension.dart │ │ │ ├── string_extension.dart │ │ │ └── string_list_extension.dart │ │ ├── http/ │ │ │ ├── controller/ │ │ │ │ ├── controller.dart │ │ │ │ └── controller_handler.dart │ │ │ ├── middleware/ │ │ │ │ ├── middleware.dart │ │ │ │ ├── middleware_handler.dart │ │ │ │ └── web_socket_middleware_handler.dart │ │ │ ├── request/ │ │ │ │ ├── request.dart │ │ │ │ ├── request_body.dart │ │ │ │ ├── request_file.dart │ │ │ │ ├── request_form_data.dart │ │ │ │ └── request_handler.dart │ │ │ ├── response/ │ │ │ │ ├── response.dart │ │ │ │ └── stream_file.dart │ │ │ ├── session/ │ │ │ │ ├── session_file_store.dart │ │ │ │ └── session_manager.dart │ │ │ └── validation/ │ │ │ ├── custom_validation_rule.dart │ │ │ ├── field_validation.dart │ │ │ ├── nested_validation.dart │ │ │ ├── rules.dart │ │ │ ├── validation_chain/ │ │ │ │ ├── export_chain_validation.dart │ │ │ │ ├── rules/ │ │ │ │ │ ├── between.dart │ │ │ │ │ ├── confirmed.dart │ │ │ │ │ ├── end_width.dart │ │ │ │ │ ├── greater_than.dart │ │ │ │ │ ├── in_array.dart │ │ │ │ │ ├── is_alpha.dart │ │ │ │ │ ├── is_alpha_dash.dart │ │ │ │ │ ├── is_alpha_numeric.dart │ │ │ │ │ ├── is_array.dart │ │ │ │ │ ├── is_boolean.dart │ │ │ │ │ ├── is_date.dart │ │ │ │ │ ├── is_double.dart │ │ │ │ │ ├── is_email.dart │ │ │ │ │ ├── is_file.dart │ │ │ │ │ ├── is_image.dart │ │ │ │ │ ├── is_integer.dart │ │ │ │ │ ├── is_ip.dart │ │ │ │ │ ├── is_json.dart │ │ │ │ │ ├── is_numeric.dart │ │ │ │ │ ├── is_required.dart │ │ │ │ │ ├── is_string.dart │ │ │ │ │ ├── is_url.dart │ │ │ │ │ ├── is_uuid.dart │ │ │ │ │ ├── lenght_between.dart │ │ │ │ │ ├── less_than.dart │ │ │ │ │ ├── max.dart │ │ │ │ │ ├── max_lenght.dart │ │ │ │ │ ├── min.dart │ │ │ │ │ ├── min_lenght.dart │ │ │ │ │ ├── not_in_array.dart │ │ │ │ │ ├── required_if.dart │ │ │ │ │ ├── required_if_not.dart │ │ │ │ │ └── start_with.dart │ │ │ │ ├── validation.dart │ │ │ │ └── validation_rule.dart │ │ │ ├── validation_item.dart │ │ │ └── validator.dart │ │ ├── ioc_container.dart │ │ ├── localization_handler/ │ │ │ └── localization.dart │ │ ├── logger/ │ │ │ └── logger.dart │ │ ├── mail/ │ │ │ ├── content.dart │ │ │ ├── envelope.dart │ │ │ ├── mail.dart │ │ │ ├── mail_view.dart │ │ │ └── mailable.dart │ │ ├── redis/ │ │ │ ├── command/ │ │ │ │ ├── client.dart │ │ │ │ ├── codec.dart │ │ │ │ └── commands.dart │ │ │ ├── exception.dart │ │ │ ├── lowlevel/ │ │ │ │ ├── protocol_client.dart │ │ │ │ └── resp.dart │ │ │ ├── redis.dart │ │ │ └── vania_redis.dart │ │ ├── route/ │ │ │ ├── middleware/ │ │ │ │ ├── csrf_middleware.dart │ │ │ │ └── throttle.dart │ │ │ ├── route.dart │ │ │ ├── route_data.dart │ │ │ ├── route_handler.dart │ │ │ ├── route_history.dart │ │ │ ├── router.dart │ │ │ ├── set_static_path.dart │ │ │ └── throttle_requests.dart │ │ ├── server/ │ │ │ ├── base_http_server.dart │ │ │ └── initialize_config.dart │ │ ├── service/ │ │ │ └── service_provider.dart │ │ ├── storage/ │ │ │ ├── local_storage.dart │ │ │ ├── s3_storage.dart │ │ │ ├── storage.dart │ │ │ └── storage_driver.dart │ │ ├── utils/ │ │ │ ├── _pluralize.dart │ │ │ ├── functions.dart │ │ │ ├── helper.dart │ │ │ └── request_helper.dart │ │ ├── view_engine/ │ │ │ ├── helper.dart │ │ │ ├── processor_engine/ │ │ │ │ ├── abs_processor.dart │ │ │ │ ├── assets_processor.dart │ │ │ │ ├── comment_processor.dart │ │ │ │ ├── csrf_processor.dart │ │ │ │ ├── csrf_token_processor.dart │ │ │ │ ├── error_processor.dart │ │ │ │ ├── evaluate_expression.dart │ │ │ │ ├── extends_processor.dart │ │ │ │ ├── for_loop_processor.dart │ │ │ │ ├── if_statement_processor.dart │ │ │ │ ├── include_processor.dart │ │ │ │ ├── old_processor.dart │ │ │ │ ├── route_processor.dart │ │ │ │ ├── section_processor.dart │ │ │ │ ├── session_processor.dart │ │ │ │ ├── switch_cases_processor.dart │ │ │ │ ├── translate_processor.dart │ │ │ │ └── variables_processor.dart │ │ │ ├── template_engine.dart │ │ │ └── template_reader.dart │ │ └── websocket/ │ │ ├── web_socket_handler.dart │ │ ├── websocket_client.dart │ │ ├── websocket_constants.dart │ │ ├── websocket_event.dart │ │ └── websocket_session.dart │ ├── vania.dart │ └── websocket.dart ├── pubspec.yaml └── test/ ├── src/ │ └── extensions/ │ ├── date_time_extension_test.dart │ ├── number_extension_test.dart │ ├── string_extension_test.dart │ └── string_list_extension_test.dart └── unit/ ├── hash_test.dart ├── route_test.dart └── validation_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve Vania title: "[BUG] - Short Description of Issue" labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Environment (please complete the following information):** - Dart Version: [e.g. 2.10.5] - Vania Version: [e.g. 1.0.3] - Operating System: [e.g. Ubuntu 20.04, Windows 10] - Database: [e.g. PostgreSQL version, MySQL version] - Any other relevant backend services or middleware **Additional context** Add any other context about the problem here. **Logs** If applicable, include logs to help diagnose the issue. ================================================ FILE: .github/ISSUE_TEMPLATE/documentation-issue.md ================================================ --- name: Documentation Issue about: Report errors, improvements, or suggestions for the documentation. title: "[Documentation] - Brief description of the issue" labels: documentation assignees: '' --- ## Description Provide a clear and concise description of the issue or suggestion with the documentation. ## Location Link to the documentation page or specify the section where the issue occurs. ## Type of Issue - [ ] Typo - [ ] Inaccuracy - [ ] Enhancement - [ ] Other ## Suggested Resolution Describe your suggested resolution, if you have one, or what you would expect to see. ## Additional Context Add any other context or screenshots about the documentation issue here. ## Would you like to work on this issue? - [ ] Yes - [ ] No ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[Feature Request] - Brief Description of Feature" labels: feature request assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/stale.yml ================================================ daysUntilStale: 60 daysUntilClose: 7 staleLabel: wontfix exemptLabels: - bug - enhancement - feature request markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. closeComment: false ================================================ FILE: .github/workflows/vania-dart.yml ================================================ name: Vania Dart on: push: branches: [ "main", "dev"] pull_request: branches: [ "main", "dev"] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Dart uses: dart-lang/setup-dart@v1.2 with: sdk: stable # Get Flutter packages - name: Install dependencies run: dart pub get # Check formatting - name: Format check run: dart format --output=none --set-exit-if-changed . # Analyze the source code - name: Analyze project source run: dart analyze # Run tests - name: Run tests run: dart test ================================================ FILE: .gitignore ================================================ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ .idea/ .vscode/ # Avoid committing pubspec.lock for library packages; see # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock ================================================ FILE: CHANGELOG.md ================================================ ## 1.1.1 - Fix(db): switch from mysql_dart to mysql_client to fix UTF-8 encoding issues - Chore update dependencies ## 1.1.0 - Add(FormValidation): add support for request validation using FormValidation - Add(RequestHelper): introduce helper methods for accessing query strings, IP, and request data - Add(Database): add database health check for non-pooled connections - Add(Validation): implement builder pattern for defining validation rules (e.g. `field('name').required().string()`) - Update(Pagination): use `getParam()` for retrieving `page` parameter in pagination - Fix(RequestFile): handle `List` stream in RequestFile.store - Improve(Exception): enhance exception handling and error messages ## 1.0.2+2 - Fix(querybuilder): fixed `postgres` params name ## 1.0.2+1 - Fix(querybuilder): remove unnecessary `update_` prefix from update bindings ## 1.0.2 - Fix(Cache): fixed cache issue - Fix(Request): fix `hasFile()` and `has()` method behavior - Add(Request): add `asList()` method to get the data as a List - Add(CSRF): add `CSRF_PROTECTION_ENABLED` config to toggle CSRF protection ## 1.0.1 - Refactor(ORM): fix eager-loading for polymorphic relations (`MorphTo`, `MorphToMany`, `MorphedByMany`) - Refactor(ORM): enhance eager-loading for core relations (`hasOne`, `hasMany`, `belongsTo`, `belongsToMany`) - Fix(Database): decode byte arrays to `UTF-8` strings in query results ## 1.0.0 - Release stable version 1.0.0 - Refactor and improve `QueryBuilder`: remove Eloquent package and implement core version - Implement full ORM functionality in core - Optimize routing system - Enhance request validation - Remove extraneous code and optimize core performance ## 0.8.2 - Fix (Session): session file locking issue on Linux. - Fix (Static File): Automatically load `index.html` if the `/` route is not defined in web routes. ## 0.8.1 - Fix(Request Validation): Custom validation rule Future - fix(upload): prevent "Exhausted heap space" error on large file uploads ## 0.8.0 - Fix session bug - Add `RedirectIfAuthenticated` Middleware - Add(Response): Back with input - Add(Request Validation): `RegExp` rule [#167](https://github.com/vania-dart/framework/issues/167) - Add(Request Validation): Custom Validation Rule - Add lockFile method for atomic session file handling - Remove Isolate from code - Refactor HttpRequest handler method to class-based structure ## 0.7.4+1 - chore: Upgraded project dependencies ## 0.7.4 - Add Route name - Add(Template engine): Route `{@ route('home') @}` , `{@ route('home', {"id":1}) @}` - Add(Template engine): assets `{@ asset('css/style.css') @}` - Fix `csrf` url excluded ## 0.7.3 - Fix Authentication bug - Refactor vania hash - Fix incoming requests bug - Fix csrf and session issue - Fix Authentication issue - Add(Template engine): `comment` tag to the template engine `{@# Comments here #@}` - Add(Template engine): translate tag to the template engine `{@ trans('welcome', {"name": "Vania"}) @}` ## 0.7.2 - Add(Template engine): error handler `hasError('email')` , `{@ error('email') @}` - Add(Template engine): session message handler `hasSession('email')` , `{@ session('success') @}` - Add(Template engine): Cross-Site Request Forgery (`CSRF`) `{@ CSRF @}` , `{@ csrf_token() @}` - Add `back()` to the response - Refactor exception handling - Refactor and sanitize route - Add Basic Auth with session to the `Authenticate` - chore: Upgraded project dependencies ## 0.7.1 - Add delete Session to the helper - Add `async-await` for all Session methods ## 0.7.0 - Feat(Session Management): Session handling capabilities to manage user sessions effectively. - Feat(Template engine): Support for rendering HTML templates using a template engine. To handle dynamic HTML rendering with support for control structures. - chore: Upgraded project dependencies ## 0.6.2 - Add redirect method [#144](https://github.com/vania-dart/framework/issues/144) - Add custom 404 error handling via HTML file [#145](https://github.com/vania-dart/framework/issues/145) ## 0.6.1 - Fix get language path ## 0.6.0 - Refactor incoming route log - Remove unnecessary library name - Add Multi-language support [#141](https://github.com/vania-dart/framework/issues/141) [Localization](https://vdart.dev/docs/the-basics/localization) ## 0.5.1 - Add support for unique constraints in migrations. Thanks to [WellingtonNico](https://github.com/WellingtonNico) for the contribution. - Refactor configuration initialization by moving database setup before service provider registration, ensuring a more reliable startup sequence. - Fix Resolved an issue with WebSocket middleware that caused unexpected behavior. See [#132](https://github.com/vania-dart/framework/issues/132) for details. - Chore Upgraded dependencies to their latest versions. ## 0.5.0 - Feat: Gate feature for defining user permissions - Add WebSocket connect, disconnect, and error handling on the server side (#126) - Add user getter method to Request class ## 0.4.3 - Fix nested JSON [#128](https://github.com/vania-dart/framework/issues/128) - Add JSON to the request `request.json()` ## 0.4.2 - Fix id auto-increment for PostgreSQL compatibility [#127](https://github.com/vania-dart/framework/issues/118) ## 0.4.1 - Refactor validation rule customErrorMessage to message - Fix JSON response for API - Fix PostgreSQL sslmode [#118](https://github.com/vania-dart/framework/issues/118) - Add enable support for list item submission `form/data` request - chore: upgrade dependencies ## 0.4.0 - Feat: a new field validation mechanism by [alirezat66](https://github.com/alirezat66) - [PR 99](https://github.com/vania-dart/framework/pull/99) - Fix nested route group [#98](https://github.com/vania-dart/framework/issues/98) - Fix middleware issue ## 0.3.5+1 - Fix send message to room ## 0.3.5 - Fix WebSocket session id - Add get room members - Add is active session - Add get active room - Add get active sessions ## 0.3.4 - Fix route camel-case issue - Add get cookie from the request ## 0.3.3+1 - Fix encoding char-set for form input handling ## 0.3.3 - Fix group route issue - Fix uuid issue (#88) - Add `Server-Sent Events (SSE)` response (#89) Thank you [Dartly](https://github.com/Dartly) ## 0.3.2 - Refactor Response class - Add jsonWithHeader response - Add QueryException to model class - Add Databse helper - Add Create and InsertMany to the ORM - Add DB Transaction - Add Cookies,Integer,asDouble to request class - Fix request body int fields - Fix PostgreSQL typo - Fix drop table issue when table has foreign key ## 0.3.1 - Fix Refresh token bug([#83](https://github.com/vania-dart/framework/issues/83)) - Fix WebSocket connect event ## 0.3.0 - Add Parameter validation conditions for the router([#79](https://github.com/vania-dart/framework/issues/79)) - Add Resource and Any route ([#80](https://github.com/vania-dart/framework/issues/80)) - Refactor Router, Route Handler - Refactor Controller handler for increasing RPS and decreasing latency - Refactor Request handler for increase RPS - Fix Null params ([#81](https://github.com/vania-dart/framework/issues/81)) ## 0.2.7 - Optimize PRS - Refactor Controller handler - Refactor Request handler - Refactor Request class - Add none to response type and await for res close - Refactor route handler - Export Database client - Add URL assets to helper ## 0.2.6 - Refactor Local storage class - Refactor Cache class - Refactor Storage class - Refactor Response class - Add AWS S3 storage driver - Add Storage env config ```env STORAGE=s3 STORAGE_S3_BUCKET='' STORAGE_S3_SECRET_KEY='' STORAGE_S3_ACCESS_KEY='' STORAGE_S3_REGION='' ``` ## 0.2.5 - Fix Authentication middleware issue - Fix static file url encoding - Add Domian to router - Add JWT env config ```env JWT_SECRET_KEY JWT_AUDIENCE JWT_ID JWT_ISSUER JWT_SUBJECT ``` ## 0.2.4 - Fix Route bug - Add WebSocket middleware - Refactor Auth middleware ## 0.2.3 - Fix Database connection issue with Isolate ## 0.2.2 - Fix Websocket Join and Left room issue([#63](https://github.com/vania-dart/framework/issues/63)) - Refactor Migration and model - Add DatabseClient class ## 0.2.1 - Fix Postgresql bug ## 0.2.0 - Add Redis (base code from dedis dart package) - Add Redis Cache Driver ## 0.1.9 - Fix Isolate bug ## 0.1.8 - Fix public and storage file path - Refactor Mailable Config to env - Refactor Migration class, created migration timestamp Add by [S.M. SHAHi](https://github.com/shahi5472) - Refactor Local cache class name to File cache ## 0.1.7+5 - Add pool and poolsize to DatabaseConfig ## 0.1.7+4 - Fix pgsql bug - Add alter column to the migration ## 0.1.7+3 - Refactor HttpException to HttpResponseException - Add abort method to the helper file ## 0.1.7+2 - Fix route group bug ## 0.1.7+1 - Fix env issue ## 0.1.7 - Add deleteTokens and deleteCurrentToken Auth class - Refactor group routing to use a callback function instead of a list - Refactor websocket data to payload - Add Middleware Handler - Fix Webscoket Route bug - Update Dependencies - Add secure bind ## 0.1.6+1 - Fix Storage issues ## 0.1.6 - Fix Websocket bugs - Refactor Storage Converted Instance Methods to Static Methods - Refactor Cache Converted Instance Methods to Static Methods ## 0.1.5+1 - Fix env issues ## 0.1.5 - Add Logger - Add env file ## 0.1.4 - Add Throttle middleware - Add move for upload file in custom folder - Add paginate and simplePagination in Eloquent ## 0.1.3 - Add mail ## 0.1.2 - Add multi-isolate server ## 0.1.1+4 - Fix Validation issue on non-required fields ## 0.1.1+3 - Add Singleton base route preFix to static - Readme file ## 0.1.1+2 - Fix bug: Cors file and class name ## 0.1.1+1 - Fix bug: http method options and cors error - ## 0.1.1 - Add Hash class ## 0.1.0 - Initial beta release - Fix a bug related to WebSocket data events - Fix authentication check functionality - Add `isAuthorized` feature - Add `query_builder` from Eloquent package for enhanced functionality ## 0.0.4 - Fix bug: Authentication refresh token ## 0.0.3+1 - Fix bug: migration columns length - Add sslmode to the MySqldriver ## 0.0.3 - Fix Bug: Resolved issue with table creation in PostgreSQL ## 0.0.2+1 - Add bigIncrements and softDeletes columns ## 0.0.2 - Add column index to vania file - Code formatted ## 0.0.1 - Initial version. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Vania ## Creating a Pull Request Before creating a pull request, please follow these steps: 1. Fork the repository and create your branch from `dev`. 2. Install all dependencies (`dart pub get`). 3. Squash your commits and ensure you have a meaningful, [semantic](https://www.conventionalcommits.org/en/v1.0.0/) commit message. 4. Add tests! Pull Requests without 100% test coverage will not be approved. 5. Ensure the existing test suite passes locally. 6. Format your code (`dart format .`). 7. Analyze your code (`dart analyze --fatal-infos --fatal-warnings .`). 8. Create the Pull Request targeting the `dev` branch. 9. Verify that all status checks are passing. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Vania Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Introduction [Vania Dart Documentation](https://vdart.dev) [Contributing to Vania](https://github.com/vania-dart/framework/blob/main/CONTRIBUTING.md) Vania is a robust backend framework designed for building high-performance web applications using Dart. With its straightforward approach and powerful features, Vania streamlines the development process for both beginners and experienced developers alike. ## Features ✅ ***Scalability***: Built to handle high traffic, Vania effortlessly scales alongside your application's growth. ✅ ***Developer-Friendly***: Enjoy an intuitive API and clear documentation, making web application development a breeze. ✅ ***Simple Routing***: Define and manage routes effortlessly with Vania's efficient routing system, ensuring robust application architecture. ✅ ***ORM Support***: Interact seamlessly with databases using Vania's powerful ORM system, simplifying data management. ✅ ***Request Data Validation***: Easily validate incoming request data to maintain data integrity and enhance security. ✅ ***Database Migration***: Manage and apply schema changes with ease using Vania's built-in database migration support. ✅ ***WebSocket***: Enable real-time communication between server and clients with WebSocket support, enhancing user experience. ✅ ***Command-Line Interface (CLI)***: Streamline development tasks with Vania's simple CLI, offering commands for creating migrations, generating models, and more. Experience the simplicity and power of Vania for your next web application project # Quick Start 🚀 Ensure that you have the [Dart SDK](https://dart.dev) installed on your machine. YouTube Video [Quick Start](https://www.youtube.com/watch?v=k8ol0F4bDKs) YouTube Video [Dart & flutter Fullstack with Vania](https://youtu.be/1tfqpusIXwQ) [![Quick Start](https://img.youtube.com/vi/k8ol0F4bDKs/0.jpg)](https://www.youtube.com/watch?v=k8ol0F4bDKs "Quick Start") ## Installing 🧑‍💻 ```shell # 📦 Install the vania cli from pub.dev dart pub global activate vania_cli ``` ## Creating a Project ✨ Use the `vania create` command to create a new project. ```shell # 🚀 Create a new project called "blog" vania create blog ``` ## Start the Dev Server 🏁 Open the newly created project and start the development server. ```shell # 🏁 Start the dev server vania serve ``` You can also include the `--vm` flag to enable VM service. ## Create a Production Build 📦 Create a production build: ```shell # 📦 Create a production build vania build ``` For production use, deploy using the provided `Dockerfile` and `docker-compose.yml` files to deploy anywhere. Example CRUD API Project [Github](https://github.com/vania-dart/example) ================================================ FILE: README.tr.md ================================================ # Introduction Vania, Dart kullanarak yüksek performanslı web uygulamaları oluşturmak için tasarlanmış sağlam bir backend çerçevesidir. Basit yaklaşımı ve güçlü özellikleriyle Vania, hem yeni başlayanlar hem de deneyimli geliştiriciler için geliştirme sürecini kolaylaştırır. ## Features ✅ ***Ölçeklenebilirlik***: Yüksek trafikle başa çıkmak için tasarlanan Vania, uygulamanızın büyümesiyle birlikte zahmetsizce kendini ölçekler. ✅ ***Geliştici Dostu***: Web uygulaması geliştirmeyi çocuk oyuncağı haline getiren bir API ve açık kaynağın keyfini çıkarın. ✅ ***Kolay Rota Oluşturma***: Vania'nın verimli yönlendirme sistemi ile rotaları zahmetsizce tanımlayın ve yönetin, sağlam bir uygulama mimarisi elde edin. ✅ ***ORM Desteği***: Vania'nın güçlü ORM sistemini kullanarak veritabanlarıyla sorunsuz bir şekilde etkileşim kurun ve veri yönetimini basitleştirin. ✅ ***İstek Verisi Doğrulama***: Veri bütünlüğünü korumak ve güvenliği artırmak için gelen talep verilerini kolayca doğrulayarak kontrol altında tutun. ✅ ***Veritabanı Yönetimi***: Vania'nın yerleşik veritabanı taşıma desteğini kullanarak şema değişikliklerini kolaylıkla yönetin ve uygulayın. ✅ ***WebSocket***: WebSocket desteği ile sunucu ve istemciler arasında gerçek zamanlı iletişim sağlayarak kullanıcı deneyimini geliştirin. ✅ ***Komut Satırı Arayüzü (CLI)***: Vania'nın veritabanı oluşturma, model oluşturma ve daha fazlası için komutlar sunan basit CLI'si ile geliştirme işlemlerini kolaylaştırın. Bir sonraki web uygulaması projeniz için Vania'nın basitliğini ve gücünü deneyimleyin Dokümantasyona buradan göz atın: [Vania Dart Dokümantasyonu](https://vdart.dev) # Hızlı Başlangıç 🚀 [Dart SDK](https://dart.dev) 'in makinenizde kurulu olduğundan emin olun. YouTube Video [Hızlı Başlangıç](https://www.youtube.com/watch?v=5LiDQlqhNto) [![Quick Start](http://img.youtube.com/vi/5LiDQlqhNto/0.jpg)](https://www.youtube.com/watch?v=5LiDQlqhNto "Hızlı Başlangıç") ## Kurulum 🧑‍💻 ```shell # 📦 pub.dev adresinden Vania CLI kurulumunu gerçekleştirin dart pub global activate vania_cli ``` ## Proje Oluşturma ✨ Oluşturmak için `vania create` komutunu kullanın. ```shell # 🚀 "blog" isminde yeni bir proje oluşturun vania create blog ``` ## Geliştirme Sunucusunu Başlatın 🏁 Yeni oluşturulan projeyi açın ve geliştirme sunucusunu başlatın. ```shell # 🏁 Sunucuyu başlat vania serve ``` Sanal Makine (VM) hizmetini etkinleştirmek için `--vm` bayrağını da ekleyebilirsiniz. ## Projeyi Derleyin 📦 Hazırladığınız projeyi derleyin ```shell # 📦 Projeyi derleyin vania build ``` Proje kullanımı için, herhangi bir yere dağıtmak üzere sağlanan `Dockerfile` ve `docker-compose.yml` dosyalarını kullanarak dağıtın. Örnek CRUD API Projesi [Github](https://github.com/vania-dart/example) ================================================ FILE: analysis_options.yaml ================================================ # This file configures the static analysis results for your project (errors, # warnings, and lints). # # This enables the 'recommended' set of lints from `package:lints`. # This set helps identify many issues that may lead to problems when running # or consuming Dart code, and enforces writing Dart using a single, idiomatic # style and format. # # If you want a smaller set of lints you can change this to specify # 'package:lints/core.yaml'. These are just the most critical lints # (the recommended set includes the core lints). # The core lints are also what is used by pub.dev for scoring packages. include: package:lints/recommended.yaml # Uncomment the following section to specify additional rules. # linter: # rules: # - camel_case_types # analyzer: # exclude: # - path/to/excluded/files/** # For more information about the core and recommended set of lints, see # https://dart.dev/go/core-lints # For additional information about configuring this file, see # https://dart.dev/guides/language/analysis-options ================================================ FILE: example/app_exception_handler.dart ================================================ import 'package:vania/src/exception/database_exception.dart'; import 'package:vania/src/exception/exception_handler.dart'; import 'package:vania/src/exception/not_found_exception.dart'; import 'package:vania/src/exception/query_exception.dart'; import 'package:vania/src/exception/validation_exception.dart'; import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/service/service_provider.dart'; import 'package:vania/application.dart'; class DatabaseExceptionHandler extends ExceptionHandler { @override Response handle(DatabaseException exception, Request? request) { return Response.json({ 'success': false, 'message': 'Database error occurred', 'error': exception.message, }, 500); } } class QueryExceptionHandler extends ExceptionHandler { @override Response handle(QueryException exception, Request? request) { return Response.json({ 'success': false, 'message': 'Query error occurred', 'error': exception.cause, }, 500); } } class NotFoundExceptionHandler extends ExceptionHandler { @override Response handle(NotFoundException exception, Request? request) { return Response.json({ 'success': false, 'message': exception.message, }, 404); } } class ValidationExceptionHandler extends ExceptionHandler { @override Response handle(ValidationException exception, Request? request) { return Response.json({ 'success': false, 'message': 'Validation failed', 'errors': exception.message, }, 422); } } class ThirdPartyExceptionHandler extends GeneralExceptionHandler { @override Response? handle(dynamic exception, Request? request) { return Response.json({ 'success': false, 'message': 'An unexpected error occurred', 'error': exception.toString(), }, 500); } } class AppExceptionServiceProvider extends ServiceProvider { @override Future boot() async {} @override Future register() async { Application().addExceptionHandlers({ DatabaseException: DatabaseExceptionHandler(), QueryException: QueryExceptionHandler(), NotFoundException: NotFoundExceptionHandler(), ValidationException: ValidationExceptionHandler(), }); Application().setGeneralExceptionHandler( ThirdPartyExceptionHandler(), ); } } ================================================ FILE: example/main.dart ================================================ import 'package:vania/vania.dart'; import 'app_exception_handler.dart'; void main() async { Application().initialize( config: { 'providers': [AppExceptionServiceProvider()], }, ); } ================================================ FILE: lib/application.dart ================================================ import 'src/container.dart'; import 'src/exception/exception_handler.dart'; import 'src/ioc_container.dart'; import 'src/localization_handler/localization.dart'; import 'src/server/base_http_server.dart'; import 'src/env_handler/env.dart'; import 'src/http/request/request_handler.dart'; import 'src/http/session/session_manager.dart'; import 'src/utils/helper.dart' show env; class Application extends Container { static Application? _singleton; final Map _exceptionHandlers = {}; GeneralExceptionHandler? _generalExceptionHandler; factory Application() { if (_singleton == null) { _singleton = Application._internal(); Env().load(); Localization().init(); } return _singleton!; } Application._internal(); void addExceptionHandler(ExceptionHandler handler) { _exceptionHandlers[T] = handler; } void addExceptionHandlers(Map handlers) { _exceptionHandlers.addAll(handlers); } void setGeneralExceptionHandler(GeneralExceptionHandler handler) { _generalExceptionHandler = handler; } ExceptionHandler? getExceptionHandler(Type type) { return _exceptionHandlers[type]; } GeneralExceptionHandler? getGeneralExceptionHandler() { return _generalExceptionHandler; } late BaseHttpServer _server; Future initialize({ required Map config, List args = const [], }) async { IoCContainer().register(() => RequestHandler()); IoCContainer().register( () => SessionManager(), singleton: true, ); if (env('APP_KEY') == '' || env('APP_KEY') == null) { throw Exception('Key not found'); } _server = BaseHttpServer(config: config, args: args); _server.startServer(); } Future close() async { _server.httpServer?.close(); } } ================================================ FILE: lib/authentication.dart ================================================ export 'src/authentication/authentication.dart'; export 'src/authentication/authenticate.dart'; export 'src/authentication/redirect_if_authenticated.dart'; export 'src/authentication/has_api_tokens.dart'; export 'src/authentication/gate/gate.dart'; ================================================ FILE: lib/database.dart ================================================ export 'src/database/seeder/seeder.dart'; export 'src/database/seeder/seeder_runner.dart'; export 'src/database/seeder/seeder_factory.dart'; export 'src/enum/column_index.dart'; ================================================ FILE: lib/http/controller.dart ================================================ export '../src/http/controller/controller.dart'; ================================================ FILE: lib/http/form_validation.dart ================================================ export '../src/contract/http/request/form_validation.dart'; ================================================ FILE: lib/http/middleware.dart ================================================ export '../src/route/middleware/throttle.dart'; export '../src/http/middleware/middleware.dart'; ================================================ FILE: lib/http/request.dart ================================================ export '../src/http/request/request.dart'; export '../src/http/request/request_file.dart'; export '../src/http/validation/validation_chain/export_chain_validation.dart'; 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'; ================================================ FILE: lib/http/response.dart ================================================ export '../src/http/response/response.dart'; export '../src/view_engine/helper.dart'; ================================================ FILE: lib/mail.dart ================================================ export 'src/mail/mailable.dart'; export 'src/mail/content.dart'; export 'src/mail/envelope.dart'; export 'src/mail/mail_view.dart'; export 'package:mailer/src/entities/address.dart'; export 'package:mailer/src/entities/attachment.dart'; ================================================ FILE: lib/migration.dart ================================================ export 'src/database/migration/runners/migration_runner.dart'; export 'src/database/migration/migration.dart'; export 'src/database/migration/builders/column_types.dart'; export 'src/database/migration/builders/schema.dart'; export 'src/database/migration/migration_connection.dart'; ================================================ FILE: lib/orm/model.dart ================================================ export '../src/database/orm/model.dart'; ================================================ FILE: lib/query_builder.dart ================================================ export 'src/database/db.dart'; export 'src/contract/database/query_builder/query_builder.dart'; export 'src/database/monitoring/database_monitor.dart'; export 'src/exception/database_exception.dart'; export 'src/exception/query_exception.dart'; export 'src/database/isolate_db.dart'; ================================================ FILE: lib/route.dart ================================================ export 'src/route/router.dart'; export 'src/route/route.dart'; ================================================ FILE: lib/service_provider.dart ================================================ export 'package:vania/src/service/service_provider.dart'; ================================================ FILE: lib/src/authentication/authenticate.dart ================================================ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:meta/meta.dart'; import 'package:vania/src/authentication/authentication.dart'; import 'package:vania/src/exception/unauthenticated.dart'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/utils/helper.dart'; class Authenticate extends Middleware { final String? guard; final bool basic; final String loginPath; Authenticate({this.guard, this.basic = false, this.loginPath = '/login'}); @mustCallSuper @override handle(Request req) async { if (basic) { bool loggedIn = await getSession('logged_in') ?? false; String guard = await getSession('auth_guard') ?? ''; if (loggedIn && guard.isNotEmpty) { Map user = await getSession?>('auth_user') ?? {}; Auth().guard(guard).login(user[guard]); } else { throw Unauthenticated( message: loginPath, responseType: ResponseType.html, ); } } else { String? token = req.header('authorization')?.replaceFirst('Bearer ', ''); try { if (guard == null) { await Auth().check(token ?? ''); } else { await Auth().guard(guard!).check(token ?? ''); } } on JWTExpiredException { throw Unauthenticated(message: 'Token expired'); } } } } ================================================ FILE: lib/src/authentication/authentication.dart ================================================ import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:vania/src/config/config.dart'; import 'package:vania/src/database/orm/model.dart'; import 'package:vania/src/exception/invalid_argument_exception.dart'; import 'package:vania/src/exception/unauthenticated.dart'; import 'package:vania/src/utils/helper.dart'; import 'has_api_tokens.dart'; import 'model/personal_access_token.dart'; class Auth { static final Auth _singleton = Auth._internal(); factory Auth() => _singleton; Auth._internal(); String _userGuard = 'default'; bool _loggedIn = false; final Map _user = {}; bool get loggedIn => _loggedIn; Map user() => _user[_userGuard]; dynamic id() => _user[_userGuard]['id'] ?? _user[_userGuard]['_id']; dynamic get(String filed) => _user[_userGuard][filed]; /// Sets the authentication guard to the specified guard name. /// /// This method changes the current user guard to the specified guard, which /// determines the user provider and authentication logic to use. If the /// specified guard is not defined in the configuration, an /// [InvalidArgumentException] is thrown. /// /// Returns the current instance of the `Auth` class. /// /// Throws: /// - [InvalidArgumentException] if the specified guard is not defined. /// Auth guard(String guard) { if (Config().get('auth')['guards'][guard] == null) { throw InvalidArgumentException('Auth guard [$guard] is not defined.'); } _userGuard = guard; return this; } /// Set the current user from a given user object. /// /// The object is expected to contain at least the `id` key. /// /// The user object will be stored in the `_user` map with the key being the /// current guard. /// /// Returns the current instance of the `Auth` class. Auth login(Map user, [bool basic = false]) { _user[_userGuard] = user; if (basic) { _updateSession(); } return this; } Future logout() async { await deleteSession('logged_in'); await deleteSession('auth_guard'); await deleteSession('auth_user'); _loggedIn = false; } /// Updates the current session with the given user and guard. /// /// The function sets the `logged_in` session key to true, and updates the /// `auth_user` and `auth_guard` session keys if they are not already set or /// have changed. The function also sets the `_isAuthorized` flag to true. /// /// The session is only updated if the user and guard have changed, and the /// function does not return anything. Future _updateSession() async { await setSession('logged_in', true); await setSession('auth_guard', _userGuard); await setSession('auth_user', _user); _loggedIn = true; } /// Create new token for the given user. /// /// The token created is a JWT token that contains the user's ID and the /// guard's name. The token is then signed with the secret key from the /// environment variable `JWT_SECRET_KEY`. /// /// If `withRefreshToken` is true, a refresh token is also created and /// returned in the `refresh_token` key of the map. /// /// If `customToken` is true, the token is not stored in the database and /// is returned as is. /// /// The `expiresIn` parameter is the duration after which the token will /// expire. If not provided, the token will expire after 1 hour. /// /// Returns a map containing the following keys: /// /// * `access_token`: the JWT token /// * `refresh_token`: the refresh token if `withRefreshToken` is true /// * `expires_in`: the duration after which the token will expire in seconds Future> createToken({ Duration? expiresIn, bool withRefreshToken = false, bool customToken = false, }) async { Map token = HasApiTokens() .setPayload(_user[_userGuard]) .createToken(_userGuard, expiresIn, withRefreshToken); if (!customToken) { await PersonalAccessToken().query.insert({ 'name': _userGuard, 'tokenable_id': _user[_userGuard]['id'], 'token': md5.convert(utf8.encode(token['access_token'])).toString(), 'created_at': DateTime.now(), }); } return token; } /// Create a new token by given refresh token. // /// The given token must be a valid refresh token. // /// The `expiresIn` parameter is the duration after which the token will /// expire. If not provided, the token will expire after 1 hour. // /// The `customToken` parameter determines if the token should be stored in /// the database or not. If `customToken` is true, the token is not stored /// in the database. // /// Returns a map containing the following keys: // /// * `access_token`: the JWT token /// * `refresh_token`: the refresh token /// * `expires_in`: the duration after which the token will expire in seconds Future> createTokenByRefreshToken( String token, { Duration? expiresIn, bool customToken = false, }) async { final newToken = HasApiTokens().refreshToken( token.replaceFirst('Bearer ', ''), _userGuard, expiresIn, ); if (!customToken) { Map payload = HasApiTokens().verify( token.replaceFirst('Bearer ', ''), _userGuard, 'refresh_token', ); Model? authenticatable = Config().get( 'auth', )['guards'][_userGuard]['provider']; if (authenticatable == null) { throw InvalidArgumentException('Authenticatable class not found'); } Map? user = await authenticatable.query .where('id', '=', payload['id']) .first(); if (user == null) { throw Unauthenticated(message: 'Invalid token'); } _user[_userGuard] = user; await PersonalAccessToken().query.insert({ 'name': _userGuard, 'tokenable_id': user['id'], 'token': md5.convert(utf8.encode(newToken['access_token'])).toString(), 'created_at': DateTime.now(), }); } return newToken; } /// Delete all the tokens for the user that is currently logged in. /// /// This is useful when a user logs out and you want to delete all of their /// tokens. /// /// Returns true if the operation was successful. Future deleteTokens(dynamic userId) async { await PersonalAccessToken().query.where('tokenable_id', '=', userId).update( {'deleted_at': DateTime.now()}, ); return true; } /// Delete the current token for the user that is currently logged in. /// /// This function marks the current token as deleted by setting the `deleted_at` /// field to the current time in the database. This operation helps to effectively /// invalidate the token. /// /// Returns a Future that resolves to true if the operation was successful. /// Future deleteCurrentToken(String token) async { await PersonalAccessToken().query .where('token', '=', md5.convert(utf8.encode(token)).toString()) .update({'deleted_at': DateTime.now()}); return true; } /// Validates and checks the provided token for authentication. /// /// This function verifies the provided JWT access token and checks its validity /// against stored personal access tokens. If the token is valid, it updates the /// token's last used timestamp and sets the current user context, marking them /// as authorized. /// /// The function handles both custom and stored tokens. For custom tokens, it /// sets the user payload directly. For stored tokens, it ensures the token exists /// and is not marked as deleted, then retrieves the associated user. /// /// Throws: /// - [Unauthenticated] if the token is invalid or not found. /// - [InvalidArgumentException] if the authenticatable provider class is not found. /// /// Returns a Future that resolves to true if the token is valid and the user is successfully authenticated. /// Future check( String token, { Map? user, bool isCustomToken = false, }) async { Map payload = HasApiTokens().verify( token.replaceFirst('Bearer ', ''), _userGuard, 'access_token', ); if (isCustomToken) { _user[_userGuard] = payload; _loggedIn = true; return true; } else { Map? exists = await PersonalAccessToken().query .where('token', '=', md5.convert(utf8.encode(token)).toString()) .whereNull('deleted_at') .first(['id']); // Throw 401 Error if token not found if (exists == null) { throw Unauthenticated(message: 'Invalid token'); } await PersonalAccessToken().query .where('token', '=', md5.convert(utf8.encode(token)).toString()) .update({'last_used_at': DateTime.now()}); if (user == null) { Model? authenticatable = Config().get( 'auth', )['guards'][_userGuard]['provider']; if (authenticatable == null) { throw InvalidArgumentException('Authenticatable class not found'); } user = await authenticatable.query .where('id', '=', payload['id']) .first(); } if (user != null) { _user[_userGuard] = user; _loggedIn = true; return true; } else { throw Unauthenticated(message: 'Invalid token'); } } } } ================================================ FILE: lib/src/authentication/gate/gate.dart ================================================ class Gate { static final Gate _instance = Gate._internal(); factory Gate() { return _instance; } Gate._internal(); /// All of the defined abilities. final Map _abilities = {}; /// Define a new ability. /// Gate().define('canDeletePost', () { /// return user.id == post.user_id; /// }); void define(String ability, Function callback) { _abilities[ability] = callback; } /// Determine if all of the given abilities should be granted for the current user. /// Gate().allows('canDeletePost'); bool allows(String ability) { final gate = _abilities[ability]; if (gate != null) { return gate(); } return false; } /// Determine if a given ability has been defined. bool has(String ability) { return _abilities.containsKey(ability); } /// Determine if any of the given abilities should be denied for the current user. bool denies(String ability) { return !allows(ability); } } ================================================ FILE: lib/src/authentication/has_api_tokens.dart ================================================ import 'dart:convert'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:vania/src/exception/unauthenticated.dart'; import 'package:vania/src/utils/helper.dart' show env; class HasApiTokens { static final HasApiTokens _singleton = HasApiTokens._internal(); factory HasApiTokens() => _singleton; HasApiTokens._internal(); Map? _userPayload = {}; HasApiTokens setPayload(Map payload) { _userPayload = payload; return this; } /// Create new token for the given user. /// /// The token created is a JWT token that contains the user's ID, the /// guard's name, and the user's payload. The token is then signed with /// the secret key from the environment variable `JWT_SECRET_KEY`. /// /// If `withRefreshToken` is true, a refresh token is also created and /// returned in the `refresh_token` key of the map. /// /// The `expiresIn` parameter is the duration after which the token will /// expire. If not provided, the token will expire after 1 hour. /// /// Returns a map containing the following keys: /// /// * `access_token`: the JWT token /// * `refresh_token`: the refresh token if `withRefreshToken` is true /// * `expires_in`: the duration after which the token will expire in seconds Map createToken([ String guard = '', Duration? expiresIn, bool withRefreshToken = false, ]) { String secretKey = env('JWT_SECRET_KEY') ?? env('APP_KEY'); Map userId = {'id': _userPayload?['id']}; if (_userPayload?['id'] == null) { userId = {'_id': _userPayload?['_id']}; } final jwt = JWT( {'user': jsonEncode(_userPayload), 'type': 'access_token', ...userId}, audience: env('JWT_AUDIENCE') == null ? null : Audience.one(env('JWT_AUDIENCE')), jwtId: env('JWT_ID'), issuer: env('JWT_ISSUER'), subject: env('JWT_SUBJECT'), ); Map payload = {}; Duration expirationTime = expiresIn ?? const Duration(hours: 1); String accessToken = jwt.sign( SecretKey('$secretKey$guard'), expiresIn: expirationTime, ); payload['access_token'] = accessToken; if (withRefreshToken) { final jwtRefresh = JWT({...userId, 'type': 'refresh_token'}); String refreshToken = jwtRefresh.sign( SecretKey('$secretKey$guard'), expiresIn: const Duration(days: 120), ); payload['refresh_token'] = refreshToken; } payload['expires_in'] = DateTime.now() .add(expirationTime) .toIso8601String(); return payload; } /// Creates a new token from a given refresh token. /// /// This function verifies the given refresh token and if it is valid, creates a /// new token using the `createToken` method. /// /// The `expiresIn` parameter is the duration after which the new token will /// expire. If not provided, the token will expire after 1 hour. /// /// Returns a map containing the following keys: /// /// * `access_token`: the new JWT token /// * `refresh_token`: the new refresh token /// * `expires_in`: the duration after which the new token will expire in seconds Map refreshToken( String token, [ String guard = '', Duration? expiresIn, ]) { final jwt = verify(token, guard, 'refresh_token'); _userPayload = jwt; return createToken(guard, expiresIn, true); } /// Verifies a given JWT token and returns the payload if it is valid. /// /// The `expectedType` parameter is the expected type of the token. If the /// token is not of this type, an `Unauthenticated` exception will be thrown. /// /// The `guard` parameter is the guard to use when verifying the token. The /// secret key will be concatenated with the guard before verifying the /// token. /// /// Returns a map containing the payload of the token if it is valid. /// /// Throws an `Unauthenticated` exception if the token is invalid or expired. Map verify(String token, String guard, String expectedType) { String secretKey = env('JWT_SECRET_KEY') ?? env('APP_KEY'); try { final jwt = JWT.verify( token, SecretKey('$secretKey$guard'), audience: env('JWT_AUDIENCE') == null ? null : Audience.one(env('JWT_AUDIENCE')), jwtId: env('JWT_ID'), issuer: env('JWT_ISSUER'), subject: env('JWT_SUBJECT'), ); final payload = jwt.payload; if (payload is! Map) { throw Unauthenticated(message: 'Invalid JWT payload type'); } if (payload['type'] != expectedType) { throw Unauthenticated(message: 'Invalid token type'); } return payload; } on JWTExpiredException { rethrow; } on JWTException { throw Unauthenticated(message: 'Invalid token'); } } } ================================================ FILE: lib/src/authentication/model/personal_access_token.dart ================================================ import 'package:vania/src/database/orm/model.dart'; class PersonalAccessToken extends Model { @override List guarded = ['id']; } ================================================ FILE: lib/src/authentication/redirect_if_authenticated.dart ================================================ import 'package:vania/src/exception/redirect_exception.dart'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/utils/helper.dart' show getSession; class RedirectIfAuthenticated extends Middleware { final String path; RedirectIfAuthenticated({required this.path}); @override Future handle(Request req) async { bool loggedIn = await getSession('logged_in') ?? false; if (loggedIn) { throw RedirectException(message: path, responseType: ResponseType.html); } } } ================================================ FILE: lib/src/aws/s3_client.dart ================================================ import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:vania/src/extensions/date_time_extension.dart'; import 'package:vania/src/utils/helper.dart'; class S3Client { static final S3Client _singleton = S3Client._internal(); factory S3Client() => _singleton; S3Client._internal(); final String _region = env('STORAGE_S3_REGION', ''); final String _bucket = env('STORAGE_S3_BUCKET', ''); final String _secretKey = env('STORAGE_S3_SECRET_KEY', ''); final String _accessKey = env('STORAGE_S3_ACCESS_KEY', ''); Uri buildUri(String key) { return Uri.https('$_bucket.s3.$_region.amazonaws.com', '/$key'); } Uint8List _hmacSha256(Uint8List key, String data) { var hmac = Hmac(sha256, key); return Uint8List.fromList(hmac.convert(utf8.encode(data)).bytes); } Uint8List _getSignatureKey( String key, String date, String regionName, String serviceName, ) { var kDate = _hmacSha256(Uint8List.fromList(utf8.encode('AWS4$key')), date); var kRegion = _hmacSha256(kDate, regionName); var kService = _hmacSha256(kRegion, serviceName); var kSigning = _hmacSha256(kService, 'aws4_request'); return kSigning; } Map generateS3Headers( String method, String key, { String? hash, }) { final algorithm = 'AWS4-HMAC-SHA256'; final service = 's3'; final dateTime = DateTime.now().toUtc().toAwsFormat(); final date = dateTime.substring(0, 8).toString(); final scope = '$date/$_region/$service/aws4_request'; final signedHeaders = 'host;x-amz-content-sha256;x-amz-date'; hash ??= sha256.convert(utf8.encode('')).toString(); final canonicalRequest = [ method, '/$key', '', 'host:$_bucket.s3.$_region.amazonaws.com', 'x-amz-content-sha256:$hash', 'x-amz-date:$dateTime', '', signedHeaders, hash, ].join('\n'); final stringToSign = [ algorithm, dateTime, scope, sha256.convert(utf8.encode(canonicalRequest)).toString(), ].join('\n'); final signingKey = _getSignatureKey(_secretKey, date, _region, service); final signature = _hmacSha256( signingKey, stringToSign, ).map((e) => e.toRadixString(16).padLeft(2, '0')).join(); final authorizationHeader = [ '$algorithm Credential=$_accessKey/$scope', 'SignedHeaders=$signedHeaders', 'Signature=$signature', ].join(', '); return { 'Authorization': authorizationHeader, 'x-amz-content-sha256': hash, 'x-amz-date': dateTime, }; } } ================================================ FILE: lib/src/cache/cache.dart ================================================ import 'package:vania/src/cache/file_cache_driver.dart'; import 'package:vania/src/utils/helper.dart' show env; import 'cache_driver.dart'; import 'redis_cache_driver.dart'; class Cache { static final Cache _singleton = Cache._internal(); factory Cache() => _singleton; Cache._internal(); final CacheDriver _driver = switch (env('CACHE_DRIVER', 'file')) { 'file' => FileCacheDriver(), 'redis' => RedisCacheDriver(), /*case 'memcached': case 'database': case 'memcache': break;*/ _ => FileCacheDriver(), }; /// set key => value to cache /// default duration is 1 hour /// ``` /// await Cache.put('foo', 'bar'); /// await Cache.put('foo', 'bar', duration: Duration(hours: 24)); /// ``` static Future put( String key, dynamic value, { Duration duration = const Duration(hours: 1), }) async { if (value == null) { throw Exception("Value can't be null"); } await Cache()._driver.put(key, value, duration: duration); } /// set key => value to cache forever /// ``` /// await Cache.forever('foo', 'bar'); /// ``` static Future forever(String key, String value) async { await Cache()._driver.forever(key, value); } /// remove a key from cache /// ``` /// await Cache.delete('foo'); /// static Future delete(String key) async { await Cache()._driver.delete(key); } /// get a value from cache /// ``` /// String? value = await Cache.get('foo'); /// static Future get(String key, [dynamic defaultValue]) async { return await Cache()._driver.get(key, defaultValue); } /// get a value exist /// ``` /// bool has = await Cache.has('foo'); /// static Future has(String key) async { return await Cache()._driver.has(key); } } ================================================ FILE: lib/src/cache/cache_driver.dart ================================================ abstract class CacheDriver { Future put(String key, dynamic value, {Duration duration}); Future forever(String key, dynamic value); Future delete(String key); Future get(String key, [dynamic defaultValue]); Future has(String key); } ================================================ FILE: lib/src/cache/file_cache_driver.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; import '../utils/helper.dart'; import 'cache_driver.dart'; class FileCacheDriver implements CacheDriver { static final FileCacheDriver _instance = FileCacheDriver._internal(); factory FileCacheDriver() => _instance; FileCacheDriver._internal(); final String _cacheDir = storagePath('framework/cache'); @override Future get(String key, [dynamic defaultValue]) async { final file = _getCacheFile(key); if (!await file.exists()) return defaultValue; try { final data = await file.readAsString(); final cache = jsonDecode(data); if (cache['expiration'] != null) { final expiration = DateTime.parse(cache['expiration']); if (DateTime.now().isAfter(expiration)) { await file.delete(); return defaultValue; } } return cache['value'] ?? defaultValue; } catch (e) { await file.delete(); return defaultValue; } } @override Future put( String key, dynamic value, { Duration duration = const Duration(hours: 1), }) async { final file = _getCacheFile(key); await _ensureCacheDirectory(); final cache = { 'value': value, 'expiration': DateTime.now().add(duration).toIso8601String(), }; await file.writeAsString(jsonEncode(cache)); } @override Future forever(String key, dynamic value) async { if (value == null) { throw Exception("Value can't be null"); } final file = _getCacheFile(key); await _ensureCacheDirectory(); final cache = {'value': value, 'expiration': null}; await file.writeAsString(jsonEncode(cache)); } @override Future has(String key) async { final value = await get(key); return value != null; } @override Future delete(String key) async { final file = _getCacheFile(key); if (!await file.exists()) return false; try { await file.delete(); return true; } catch (e) { return false; } } File _getCacheFile(String key) { final fileName = base64Url.encode(utf8.encode(key)); return File(path.join(_cacheDir, fileName)); } Future _ensureCacheDirectory() async { final dir = Directory(_cacheDir); if (!await dir.exists()) { await dir.create(recursive: true); } } } ================================================ FILE: lib/src/cache/redis_cache_driver.dart ================================================ import 'package:vania/src/redis/vania_redis.dart'; import 'package:vania/src/utils/helper.dart' show env; import 'cache_driver.dart'; class RedisCacheDriver extends CacheDriver { String prefix = env('REDIS_PREFIX', '${env('APP_NAME', 'vania')}_database_'); @override Future delete(String key) async { await Redis().initialized; await Redis().command.del('$prefix$key'); } @override Future forever(String key, value) async { await Redis().initialized; await Redis().command.set('$prefix$key', value); } @override Future get(String key, [defaultValue]) async { await Redis().initialized; return await Redis().command.get('$prefix$key') ?? defaultValue; } @override Future has(String key) async { await Redis().initialized; return await Redis().command.exists('$prefix$key'); } @override Future put(String key, value, {Duration? duration}) async { duration ??= Duration(hours: 24); await Redis().initialized; await Redis().command.setEx('$prefix$key', duration.inSeconds, value); } } ================================================ FILE: lib/src/config/config.dart ================================================ class Config { static final Config _singleton = Config._internal(); factory Config() => _singleton; Config._internal(); Map _config = {}; set setApplicationConfig(Map conf) => _config = conf; dynamic get(String key) => _config[key]; } class CORSConfig { final bool enabled; final dynamic origin; final dynamic methods; final dynamic headers; final dynamic exposeHeaders; final bool? credentials; final num? maxAge; const CORSConfig({ this.enabled = true, this.origin, this.methods, this.headers, this.exposeHeaders, this.credentials, this.maxAge, }); } ================================================ FILE: lib/src/config/defined_regexp.dart ================================================ final ipRegExp = RegExp( r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', ); final alphaDashRegExp = RegExp(r'^[a-zA-Z-_]+$'); final alphaNumericRegExp = RegExp(r'^[a-zA-Z0-9]+$'); final alphaRegExp = RegExp(r'^[a-zA-Z]+$'); final emailRegExp = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'); final uuidRegExp = RegExp( r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', ); ================================================ FILE: lib/src/config/http_cors.dart ================================================ import 'dart:io'; import 'config.dart'; class HttpCors { HttpCors(HttpRequest req) { CORSConfig? cors = Config().get('cors'); if (cors != null && cors.enabled) { Map headers = { HttpHeaders.accessControlAllowOriginHeader: cors.origin, HttpHeaders.accessControlAllowMethodsHeader: cors.methods, HttpHeaders.accessControlAllowHeadersHeader: cors.headers, HttpHeaders.accessControlExposeHeadersHeader: cors.exposeHeaders, HttpHeaders.accessControlAllowCredentialsHeader: cors.credentials, HttpHeaders.accessControlMaxAgeHeader: cors.maxAge, }; headers.forEach((String key, dynamic value) { _setCorsValue(req.response, key, value); }); } } // set cors in header void _setCorsValue(HttpResponse res, String key, dynamic data) { if (data == null) { return; } /// when data is list of string, eg. ['GET', 'POST'] if (data is List && data.isNotEmpty) { res.headers.add(key, data.join(',')); return; } /// when data is string, eg. 'GET' if (data is String && data.isNotEmpty) { res.headers.add(key, data); return; } /// when data is other type and has value, just convert to string if (data != null) { res.headers.add(key, data.toString()); } } } ================================================ FILE: lib/src/container.dart ================================================ class Container {} ================================================ FILE: lib/src/contract/database/_connectors/_database_connection.dart ================================================ abstract interface class DatabaseConnection { Future connect(); Future>> select( String query, [ Map bindings = const {}, ]); Future insert(String query, [Map bindings = const {}]); Future execute( String query, [ Map bindings = const {}, ]); Future transaction(Future Function() action); Future close(); } ================================================ FILE: lib/src/contract/database/query_builder/_bulk_operations_builder.dart ================================================ part of 'query_builder.dart'; enum ConflictAction { update, ignore, replace, delete } abstract class BulkOperationsBuilder { Future merge( List> sourceData, { required List matchOn, ConflictAction whenMatched = ConflictAction.update, ConflictAction whenNotMatched = ConflictAction.ignore, ConflictAction? whenNotMatchedBySource, List? updateColumns, List? insertColumns, Map? additionalValues, }); Future bulkInsert( List> data, { ConflictAction conflictAction = ConflictAction.ignore, List? conflictColumns, List? updateColumns, int batchSize = 1000, bool returnIds = false, }); Future bulkUpdate( List> updates, { required String matchColumn, List? updateColumns, int batchSize = 500, Map? additionalValues, }); Future bulkDelete({ String? column, List? values, int batchSize = 1000, }); Future bulkDeleteWhere( List> conditions, { int batchSize = 500, }); Future batchProcess({ required int batchSize, required Future Function( List> batch, int batchNumber, ) processor, List columns = const ['*'], }); Future chunkedProcess({ required int chunkSize, required Future>> Function( List> chunk, ) processor, String? destination, List columns = const ['*'], }); Future parallelBulkInsert( List> data, { int parallelism = 2, int batchSize = 1000, ConflictAction conflictAction = ConflictAction.ignore, List? conflictColumns, }); Future transactionalBulkOperation(Future Function() operations); } ================================================ FILE: lib/src/contract/database/query_builder/_cte_builder.dart ================================================ part of 'query_builder.dart'; abstract class CteBuilder { QueryBuilder withCte( String name, QueryBuilder subQuery, { List? columns, }); QueryBuilder withMultiple( Map ctes, { Map>? columnsMap, }); QueryBuilder withRecursive( String name, QueryBuilder baseCase, QueryBuilder recursiveCase, { List? columns, }); QueryBuilder withMaterialized( String name, QueryBuilder subQuery, { List? columns, }); QueryBuilder withNotMaterialized( String name, QueryBuilder subQuery, { List? columns, }); } ================================================ FILE: lib/src/contract/database/query_builder/_delete_query_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class DeleteQueryBuilder { Future delete(); Future truncate({bool force = false}); } ================================================ FILE: lib/src/contract/database/query_builder/_insert_query_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class InsertQueryBuilder { Future insert(Map values); Future insertGetId(Map values, [String? sequence]); Future insertMany(List> values); Future insertOrIgnore(Map values); Future insertUsing(List columns, QueryBuilder subQuery); Future upsert( Map values, List uniqueBy, [ Map? update, ]); } ================================================ FILE: lib/src/contract/database/query_builder/_join_clause_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class JoinClauseBuilder { QueryBuilder crossJoin(String table, [List bindings = const []]); QueryBuilder join( String table, String firstColumn, [ String? operator, String? secondColumn, String type = 'inner', bool where = false, ]); QueryBuilder joinSub( QueryBuilder subQuery, String as, String firstColumn, [ String? operator, String? secondColumn, String type = 'inner', ]); QueryBuilder leftJoin( String table, String firstColumn, [ String? operator, String? secondColumn, bool where = false, ]); QueryBuilder leftJoinSub( QueryBuilder subQuery, String as, String firColumnst, [ String? operator, String? secondColumn, ]); QueryBuilder rightJoin( String table, String firstColumn, [ String? operator, String? secondColumn, ]); } ================================================ FILE: lib/src/contract/database/query_builder/_query_executor_builder.dart ================================================ part of 'query_builder.dart'; abstract class QueryExecutorBuilder { Future avg(String column); Future chunk( int chunk, void Function(List> data) callback, ); Future chunkById( int chunk, void Function(List> data) callback, [ String column, ]); Future count([String columns = '*']); Future doesntExist(); Future each(void Function(Map) callback); Future exists(); Future?> find( dynamic id, { String byColumnName = 'id', List columns = const ['*'], }); Future?> findOrFail( dynamic id, { String byColumnName = 'id', List columns = const ['*'], }); Future?> first([List columns]); Future?> firstOrFail([ List columns = const ['*'], ]); Future?> firstWhere( String column, [ String? operator, dynamic value, List columns = const ['*'], ]); Future>> get([List columns]); Stream>> lazy([ int chunk = 1000, String column, ]); Stream> cursor([int chunk = 1000]); Future max(String column); Future min(String column); Future> paginate({ int perPage = 15, List columns = const ['*'], String? pageName, int? page, }); Future pluck(String column, [String? key]); Future> simplePaginate([ int perPage, List columns, String pageName, int? page, ]); Future sum(String column); Future value(String column); } ================================================ FILE: lib/src/contract/database/query_builder/_select_query_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class SelectQueryBuilder { QueryBuilder addSelect(List columns); QueryBuilder select([List columns]); QueryBuilder selectRaw(String query, [List bindings = const []]); QueryBuilder selectSub(QueryBuilder subQuery, String as); } ================================================ FILE: lib/src/contract/database/query_builder/_union_clause_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class UnionClauseBuilder { QueryBuilder union(QueryBuilder query); QueryBuilder unionAll(QueryBuilder query); } ================================================ FILE: lib/src/contract/database/query_builder/_update_query_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class UpdateQueryBuilder { Future decrement( String column, [ int amount = 1, Map extra = const {}, ]); Future increment( String column, [ int amount = 1, Map extra = const {}, ]); Future incrementEach( Map increments, [ Map extra = const {}, ]); Future update(Map values); Future updateMany(List> updates, String column); Future updateOrInsert( Map search, Map update, ); } ================================================ FILE: lib/src/contract/database/query_builder/_where_clauses_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class WhereClausesBuilder { QueryBuilder orWhere( dynamic condition, [ String operator = '=', dynamic value, String boolean = 'and', ]); QueryBuilder orWhereBetween(String column, List values, {bool not = false}); QueryBuilder orWhereColumn( String first, String operator, String secondColumn, ); QueryBuilder orWhereDate(String column, String operator, dynamic value); QueryBuilder orWhereDay(String column, String operator, dynamic value); QueryBuilder orWhereExists(QueryCallback callback, {bool not = false}); QueryBuilder orWhereFullText( dynamic columns, dynamic query, [ Map options = const {}, ]); QueryBuilder orWhereHour(String column, String operator, dynamic value); QueryBuilder orWhereIn(String column, List values, {bool not = false}); QueryBuilder orWhereJsonContains( String column, dynamic value, { bool not = false, }); QueryBuilder orWhereJsonDoesntContain(String column, dynamic value); QueryBuilder orWhereJsonLength(String column, String operator, dynamic value); QueryBuilder orWhereLike( String column, dynamic value, { bool caseSensitive = false, }); QueryBuilder orWhereMonth(String column, String operator, dynamic value); QueryBuilder orWhereNotBetween(String column, List values); QueryBuilder orWhereNotExists(QueryCallback callback); QueryBuilder orWhereNotIn(String column, List values); QueryBuilder orWhereNotLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }); QueryBuilder orWhereNotNull(String column); QueryBuilder orWhereNull(String column); QueryBuilder orWhereRaw(String sql, [List bindings = const []]); QueryBuilder orWhereRowValues( List columns, String operator, List values, ); QueryBuilder orWhereTime(String column, String operator, dynamic value); QueryBuilder orWhereYear(String column, String operator, dynamic value); QueryBuilder where( dynamic condition, [ String operator = '=', dynamic value, String boolean = 'and', ]); QueryBuilder whereAfterToday(String column, {String boolean = 'and'}); QueryBuilder whereAll( String column, List values, { String boolean = 'and', }); QueryBuilder whereAny( String column, List values, { String boolean = 'and', }); QueryBuilder whereBeforeToday(String column, {String boolean = 'and'}); QueryBuilder whereBetween( String column, List values, { String boolean = 'and', bool not = false, }); QueryBuilder whereBetweenColumns( String column, List columns, { String boolean = 'and', }); QueryBuilder whereColumn( String firstColumn, String operator, String secondColumn, [ String boolean = 'and', ]); QueryBuilder whereDate( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereDay( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereEqualTo( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereExists( QueryCallback callback, { String boolean = 'and', bool not = false, }); QueryBuilder whereFullText( dynamic columns, dynamic query, [ Map options = const {}, ]); QueryBuilder whereFuture(String column, {String boolean = 'and'}); QueryBuilder whereGreaterThan( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereGreaterThanOrEqualTo( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereHour( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereIn( String column, List values, { String boolean = 'and', bool not = false, }); QueryBuilder whereJsonContains( String column, dynamic value, { String boolean = 'and', bool not = false, }); QueryBuilder whereJsonDoesntContain( String column, dynamic value, { String boolean = 'and', }); QueryBuilder whereJsonLength( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereLessThan( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereLessThanOrEqualTo( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }); QueryBuilder whereMonth( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereNone( String column, List values, { String boolean = 'and', }); QueryBuilder whereNotBetween( String column, List values, { String boolean = 'and', }); QueryBuilder whereNotBetweenColumns( String column, List columns, { String boolean = 'and', }); QueryBuilder whereNotEqualTo( dynamic condition, [ dynamic value, String boolean = 'and', ]); QueryBuilder whereNotExists(QueryCallback callback, {String boolean = 'and'}); QueryBuilder whereNotIn(String column, List values, {String boolean = 'and'}); QueryBuilder whereNotLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }); QueryBuilder whereNotNull(String column, {String boolean = 'and'}); QueryBuilder whereNowOrFuture(String column, {String boolean = 'and'}); QueryBuilder whereNowOrPast(String column, {String boolean = 'and'}); QueryBuilder whereNull( String column, { String boolean = 'and', bool not = false, }); QueryBuilder wherePast(String column, {String boolean = 'and'}); QueryBuilder whereRaw( String sql, [ List bindings = const [], String boolean = 'and', ]); QueryBuilder whereRowValues( List columns, String operator, List values, { String boolean = 'and', }); QueryBuilder whereTime( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereToday(String column, {String boolean = 'and'}); QueryBuilder whereTodayOrAfter(String column, {String boolean = 'and'}); QueryBuilder whereTodayOrBefore(String column, {String boolean = 'and'}); QueryBuilder whereYear( String column, String operator, dynamic value, { String boolean = 'and', }); QueryBuilder whereHas( String relation, QueryCallback callback, { String boolean = 'and', }); QueryBuilder orWhereHas(String relation, QueryCallback callback); QueryBuilder whereDoesntHave( String relation, QueryCallback callback, { String boolean = 'and', }); QueryBuilder orWhereDoesntHave(String relation, QueryCallback callback); QueryBuilder withSoftDeletes([String column = 'deleted_at']); } ================================================ FILE: lib/src/contract/database/query_builder/_window_functions_builder.dart ================================================ part of 'query_builder.dart'; abstract interface class WindowFunctionsBuilder { QueryBuilder rowNumber({String? partitionBy, String? orderBy, String? as}); QueryBuilder rank({String? partitionBy, String? orderBy, String? as}); QueryBuilder denseRank({String? partitionBy, String? orderBy, String? as}); QueryBuilder lag( String column, { int offset = 1, dynamic defaultValue, String? partitionBy, String? orderBy, String? as, }); QueryBuilder lead( String column, { int offset = 1, dynamic defaultValue, String? partitionBy, String? orderBy, String? as, }); QueryBuilder firstValue( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder lastValue( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder ntile( int buckets, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder percentRank({String? partitionBy, String? orderBy, String? as}); QueryBuilder cumeDist({String? partitionBy, String? orderBy, String? as}); QueryBuilder windowSum( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder windowAvg( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder windowCount( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder windowMax( String column, { String? partitionBy, String? orderBy, String? as, }); QueryBuilder windowMin( String column, { String? partitionBy, String? orderBy, String? as, }); } ================================================ FILE: lib/src/contract/database/query_builder/query_builder.dart ================================================ import 'package:meta/meta.dart'; import '../../../database/_connection_manager.dart'; import '../../../database/monitoring/database_monitor.dart'; import '../../../exception/invalid_argument_exception.dart'; import '../_connectors/_database_connection.dart'; part '../../../database/_database_utils/_paginated_result.dart'; part '../../../database/_database_utils/_raw_expression.dart'; part '_bulk_operations_builder.dart'; part '_cte_builder.dart'; part '_delete_query_builder.dart'; part '_insert_query_builder.dart'; part '_join_clause_builder.dart'; part '_query_executor_builder.dart'; part '_select_query_builder.dart'; part '_union_clause_builder.dart'; part '_update_query_builder.dart'; part '_where_clauses_builder.dart'; part '_window_functions_builder.dart'; typedef QueryCallback = QueryBuilder Function(QueryBuilder qb); abstract class QueryBuilder implements InsertQueryBuilder, UpdateQueryBuilder, WhereClausesBuilder, SelectQueryBuilder, DeleteQueryBuilder, UnionClauseBuilder, JoinClauseBuilder, QueryExecutorBuilder, WindowFunctionsBuilder, CteBuilder, BulkOperationsBuilder { @protected final List conditions = []; @protected final Map bindings = {}; @protected List selectColumns = []; @protected List joins = []; @protected List unions = []; @protected String build({String? aggregateFunction, String? aggregateColumn}); String toSql(); String toRawSql(); @protected DatabaseConnection? get dbConnection => ConnectionManager().connection(connectionName); @protected String get getTable => ''; String? get connectionName; Future getConnection() async { if (!ConnectionManager().isConnected) { throw InvalidArgumentException('No database connection found.'); } return dbConnection!; } QueryBuilder connection([String? connection]); QueryBuilder table(String table, [String? as]); RawExpression raw(String value); Future transaction( Future Function() action, [ String? conditionName, ]); Stream alerts(); Map getPerformanceStats(); Map getBindings() { return bindings; } @protected String buildJoins() { return joins.isNotEmpty ? " ${joins.join(" ")}" : ""; } @protected String buildWhereClause() { return conditions.isNotEmpty ? "WHERE ${conditions.join(" ")}" : ""; } @protected String formatValue(dynamic value) { if (value is num) return value.toString(); return "'$value'"; } QueryBuilder groupBy(List groups); QueryBuilder having( String column, [ String? operator, dynamic value, String boolean = 'and', ]); QueryBuilder havingBetween( String column, List values, { String boolean = 'and', bool not = false, }); QueryBuilder inRandomOrder([dynamic seed]); QueryBuilder latest([String column = 'created_at']); QueryBuilder limit(int value); QueryBuilder offset(int value); QueryBuilder orderBy(String column, [String direction = 'ASC']); QueryBuilder orderByAsc(String column); QueryBuilder orderByDesc(String column); QueryBuilder reorder([String? column, String? direction]); QueryBuilder skip(int value); QueryBuilder take(int value); } ================================================ FILE: lib/src/contract/http/request/form_validation.dart ================================================ import '../../../http/validation/custom_validation_rule.dart'; abstract class FormValidation { dynamic rules(); List customRule() => []; Map messages() => {}; bool authorize() => true; } ================================================ FILE: lib/src/contract/orm/morph_relation.dart ================================================ import 'package:vania/src/contract/orm/relation.dart'; abstract class MorphRelation extends Relation { final String morphKey; final String morphType; final String? type; final String? pivotTable; final String? relatedMorphKey; MorphRelation({ required super.related, required super.parent, required this.morphKey, required this.morphType, this.pivotTable, this.relatedMorphKey, super.localKey = 'id', this.type, }); } ================================================ FILE: lib/src/contract/orm/relation.dart ================================================ import '../../database/orm/model.dart'; abstract class Relation { final Model related; final Model parent; String? foreignKey; String localKey; Relation({ required this.related, required this.parent, this.foreignKey, this.localKey = 'id', }); Map>> buildDictionary( List> results, String key, ) { Map>> dictionary = {}; for (var result in results) { String modelKey = result[key].toString(); if (!dictionary.containsKey(modelKey)) { dictionary[modelKey] = []; } dictionary[modelKey]!.add(result); } return dictionary; } List> match( List> models, List> results, String relation, ); List> matchOneOrMany( List> models, List> results, String relation, String parentKey, String relatedKey, ) { Map> dictionary = {}; for (var result in results) { String key = result[relatedKey].toString(); dictionary[key] = result; } models = models.map((model) { String key = model[parentKey].toString(); Map cloneModel = Map.from(model); if (dictionary.containsKey(key)) { cloneModel[relation] = dictionary[key]; } else { cloneModel[relation] = null; } return cloneModel; }).toList(); return models; } List> matchToMany( List> parents, List> results, String relation, String parentLocalKey, String parentPivotKey, String relatedPivotKey, { List pivotFields = const [], }) { final lookup = >>{}; for (var row in results) { final pivotVal = row[parentPivotKey]; lookup.putIfAbsent(pivotVal, () => []).add(row); } return parents.map((parent) { final cloned = Map.from(parent); final primaryVal = parent[parentLocalKey]; final joinedRows = lookup[primaryVal] ?? []; final relatedList = joinedRows.map((row) { final relatedData = Map.from(row) ..remove(parentPivotKey) ..remove(relatedPivotKey) ..removeWhere((k, _) => pivotFields.contains(k)); return relatedData; }).toList(); cloned[relation] = relatedList; return cloned; }).toList(); } /// Match many related models to parents (for normal relations) List> matchMany( List> models, List> results, String relation, String parentKey, String relatedKey, ) { if (results.isEmpty || models.isEmpty) { return models; } Map>> dictionary = {}; for (var result in results) { final key = result[relatedKey]; if (!dictionary.containsKey(key)) { dictionary[key] = []; } dictionary[key]!.add(result); } models = models.map>((Map model) { final key = model[parentKey]; Map cloneModel = Map.from(model); if (dictionary.containsKey(key)) { cloneModel[relation] = dictionary[key]; } else { cloneModel[relation] = []; } return cloneModel; }).toList(); return models; } /// Match many related morph models to parents. /// Only includes results where the morph type (at column [typeKey]) equals [expectedType]. List> matchMorphMany( List> models, List> results, String relation, String parentKey, String relatedIdKey, String typeKey, String expectedType, ) { Map>> dictionary = {}; for (var result in results) { if (result[typeKey]?.toString() == expectedType) { String key = result[relatedIdKey].toString(); if (!dictionary.containsKey(key)) { dictionary[key] = []; } dictionary[key]!.add(result); } } models = models.map((model) { String key = model[parentKey].toString(); Map cloneModel = Map.from(model); if (dictionary.containsKey(key)) { cloneModel[relation] = dictionary[key]; } else { cloneModel[relation] = >[]; } return cloneModel; }).toList(); return models; } List> matchMorphToOne( List> parents, List> results, String relation, String morphKey, String relatedKey, ) { final dict = >{}; for (var row in results) { final key = row[relatedKey].toString(); dict[key] = row; } return parents.map((parent) { final clone = Map.from(parent); final lookupKey = parent[morphKey]?.toString(); clone[relation] = lookupKey != null ? dict[lookupKey] : null; return clone; }).toList(); } /// Match one related morph model to parents (for hasOne polymorphic relation). /// Only includes results where [typeKey] equals [expectedType] and returns only the first matching result. List> matchMorphOneOrMany( List> models, List> results, String relation, String parentKey, String relatedIdKey, String typeKey, String expectedType, ) { Map> dictionary = {}; for (var result in results) { if (result[typeKey]?.toString().toLowerCase() == expectedType) { String key = result[relatedIdKey].toString(); if (!dictionary.containsKey(key)) { dictionary[key] = result; } } } models = models.map((model) { String key = model[parentKey].toString(); Map cloneModel = Map.from(model); if (dictionary.containsKey(key)) { cloneModel[relation] = dictionary[key]; } else { cloneModel[relation] = null; } return cloneModel; }).toList(); return models; } } ================================================ FILE: lib/src/cryptographic/hash.dart ================================================ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:vania/src/utils/helper.dart' show env; class Hash { static final Hash _singleton = Hash._internal(); factory Hash() => _singleton; Hash._internal(); String? _hashKey; Hash setHashKey(String hashKey) { _hashKey = hashKey; return this; } /// Generates a hashed password using PBKDF2. /// /// This method creates a unique salt and uses it along with the given /// password to generate a hash using the PBKDF2 algorithm. The resulting /// hashed password is a concatenation of the salt and the hash. /// /// Returns a string containing the salt followed by the hash. String make(String password) { String salt = _generateSalt(); String hash = _hashPbkdf2(password, salt); String hashedPassword = salt + hash; return hashedPassword; } /// Verifies that the given [plainPassword] matches the given [hashedPassword]. /// /// This method works by first extracting the salt from the given [hashedPassword] /// and then using the extracted salt and the given [plainPassword] to generate /// a hash using the PBKDF2 algorithm. The resulting hash is then compared to /// the given [hashedPassword] to check if it matches. /// /// Returns true if the given [plainPassword] matches the given [hashedPassword], /// false otherwise. bool verify(String plainPassword, String hashedPassword) { int saltLength = 4; String salt = hashedPassword.substring(0, saltLength); String hash = _hashPbkdf2(plainPassword, salt); String saltHash = salt + hash; return _hashEquals(saltHash, hashedPassword); } String _generateSalt() { const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; final random = Random(); return String.fromCharCodes( Iterable.generate( 4, (_) => charset.codeUnitAt(random.nextInt(charset.length)), ), ); } /// Hashes the given [password] using the given [salt] and the APP_KEY or /// the given hash key. /// /// The method works by first encoding the given [salt] and [password] into bytes. /// These bytes are then used to compute a SHA-512 HMAC using the bytes of /// the hash key or the APP_KEY if no hash key is given. /// /// The resulting HMAC bytes are then encoded using Base64 and returned as a /// string. String _hashPbkdf2(String password, String salt) { var bytes = utf8.encode(salt + password); var hmac = Hmac(sha512, utf8.encode(_hashKey ?? env('APP_KEY'))); return base64.encode(hmac.convert(bytes).bytes); } /// Compares two strings in a timing-safe manner to prevent timing attacks. /// /// This method works by first checking if the lengths of the two strings /// are equal. If they are not, the method immediately returns false. /// /// If the lengths are equal, the method then compares the individual characters /// of the two strings. If any of the characters are not equal, the method /// immediately returns false. /// /// If all characters are equal, the method returns true. bool _hashEquals(String salt, String hashedPassword) { if (salt.length != hashedPassword.length) { return false; } var result = 0; for (int i = 0; i < salt.length; i++) { result |= salt.codeUnitAt(i) ^ hashedPassword.codeUnitAt(i); } return result == 0; } } ================================================ FILE: lib/src/cryptographic/vania_encryption.dart ================================================ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; class VaniaEncryption { static final List _fixedNonce = List.filled(12, 0); /// Encrypts the given [plainText] using the provided [passphrase]. /// /// This method first encodes the [plainText] using Base64 and `UTF-8` encoding. /// Then, it creates a cryptographic key from the [passphrase] and uses the /// AES encryption algorithm to encrypt the text with a predefined initialization /// vector (IV). The result is an encrypted string returned in Base64 format. /// /// Parameters: /// - [plainText]: The text to be encrypted. /// - [passphrase]: The passphrase used to generate the encryption key. /// /// Returns: /// A Base64 encoded string representing the encrypted text. static Future encryptString( String plainText, String passphrase, ) async { try { plainText = base64.encode(utf8.encode(plainText)); final keyBytes = utf8.encode( passphrase.padRight(32, '0').substring(0, 32), ); final secretKey = SecretKey(keyBytes); final plainBytes = utf8.encode(plainText); final aesGcm = AesGcm.with256bits(); final secretBox = await aesGcm.encrypt( plainBytes, secretKey: secretKey, nonce: _fixedNonce, ); final combined = []; combined.addAll(secretBox.nonce); combined.addAll(secretBox.cipherText); combined.addAll(secretBox.mac.bytes); return base64.encode(combined); } catch (error) { return ''; } } /// Decrypts the given [encryptedText] using the provided [passphrase]. /// /// This method first creates a cryptographic key from the [passphrase]. /// It then uses the AES encryption algorithm to decrypt the [encryptedText] /// with a predefined initialization vector (IV). The decrypted text is /// decoded from Base64 and `UTF-8` encoding to return the original plaintext. /// /// Parameters: /// - [encryptedText]: The text to be decrypted, in Base64 format. /// - [passphrase]: The passphrase used to generate the decryption key. /// /// Returns: /// The original plaintext if decryption is successful, or an empty /// string if decryption fails. static Future decryptString( String encryptedText, String passphrase, ) async { try { final keyBytes = utf8.encode( passphrase.padRight(32, '0').substring(0, 32), ); final secretKey = SecretKey(keyBytes); // Decode the base64 encrypted text final encryptedBytes = base64.decode(encryptedText); final nonce = encryptedBytes.sublist(0, 12); final mac = encryptedBytes.sublist(encryptedBytes.length - 16); final cipherText = encryptedBytes.sublist(12, encryptedBytes.length - 16); final secretBox = SecretBox(cipherText, nonce: nonce, mac: Mac(mac)); final aesGcm = AesGcm.with256bits(); final decryptedBytes = await aesGcm.decrypt( secretBox, secretKey: secretKey, ); final decryptedText = utf8.decode(decryptedBytes); return utf8.decode(base64.decode(decryptedText)); } catch (error) { return ''; } } } ================================================ FILE: lib/src/database/_connection_manager.dart ================================================ import 'dart:async'; import 'package:vania/src/exception/database_exception.dart'; import '../contract/database/_connectors/_database_connection.dart'; import '../exception/invalid_argument_exception.dart'; import '../logger/logger.dart'; import '_connectors/_database_connection_factory.dart'; import '_connectors/_database_connection_proxy.dart'; import '_database_utils/_db_config.dart'; import 'monitoring/database_monitor.dart'; class ConnectionManager { static ConnectionManager? _singleton; final DatabaseMonitor _monitor = DatabaseMonitor(); factory ConnectionManager() { _singleton ??= ConnectionManager._internal(); return _singleton!; } ConnectionManager._internal(); Map connectionMap = {}; String? defaultConnection; bool get isConnected => connectionMap.isNotEmpty; DatabaseConnection? connection([String? connectionName]) { if (connectionName == null || connectionName.isEmpty) { connectionName = defaultConnection; } return connectionMap[connectionName]; } Future connect(DBConfig config, String connectionName) async { try { final connection = DatabaseConnectionFactory.createConnection(config); await connection.connect(); final monitoredConnection = DatabaseConnectionProxy( connection, connectionName, _monitor, ); connectionMap[connectionName] = monitoredConnection; if (!config.pool) { await _checkDatabaseHealth(monitoredConnection); } } on InvalidArgumentException catch (e) { Logger.log(e.message, type: Logger.ERROR); throw DatabaseException("Failed to connect to the database", e); } } Future _checkDatabaseHealth(DatabaseConnection connection) async { Timer.periodic(Duration(minutes: 5), (timer) async { try { await connection.execute('SELECT 1;'); } catch (_) {} }); } Future transaction( Future Function() action, [ String? connectionName, ]) async { final conn = connectionName ?? defaultConnection; if (conn == null) { throw DatabaseException("No connection specified for transaction"); } DatabaseConnection? transactionConnection; transactionConnection = connection(connectionName); if (transactionConnection == null) { throw DatabaseException("Connection not found for transaction"); } return await transactionConnection.transaction(action); } Stream get alerts => _monitor.alerts; Map getPerformanceStats() => _monitor.getPerformanceStats(); } ================================================ FILE: lib/src/database/_connectors/_database_connection_factory.dart ================================================ import '../../exception/invalid_argument_exception.dart'; import '../_database_utils/_db_config.dart'; import '../../contract/database/_connectors/_database_connection.dart'; import '../adapters/_mysql_connector.dart'; import '../adapters/_postgres_connector.dart'; import '../adapters/_sqlite_connector.dart'; class DatabaseConnectionFactory { static DatabaseConnection createConnection(DBConfig config) { return switch (config.driver) { 'mysql' => MySqlConnector(config), 'pgsql' => PostgresConnector(config), 'sqlite' => SQLiteConnector(config), _ => throw InvalidArgumentException( "Unsupported driver [${config.driver}].", ), }; } } ================================================ FILE: lib/src/database/_connectors/_database_connection_proxy.dart ================================================ import 'package:vania/src/contract/database/_connectors/_database_connection.dart'; import 'package:vania/src/exception/database_exception.dart' show DatabaseException; import '../../logger/logger.dart'; import '../monitoring/database_monitor.dart'; class DatabaseConnectionProxy implements DatabaseConnection { final DatabaseConnection _connection; final DatabaseMonitor _monitor; final String _connectionId; DatabaseConnectionProxy(this._connection, this._connectionId, this._monitor); // Expose the underlying connection for pool management DatabaseConnection get underlyingConnection => _connection; @override Future connect() => _connection.connect(); @override Future close() => _connection.close(); String _formatQuery(String? query, Map bindings) { if (query == null) { Logger.log( 'Warning: Null query received in _formatQuery', type: Logger.WARNING, ); return 'INVALID QUERY: NULL'; } try { var formattedQuery = query; if (bindings.isEmpty) { return formattedQuery; } final sortedBindings = bindings.entries.toList() ..sort((a, b) => b.key.length.compareTo(a.key.length)); for (var entry in sortedBindings) { final placeholder = ':${entry.key}'; final value = entry.value; final formattedValue = _formatValue(value); formattedQuery = formattedQuery.replaceAll(placeholder, formattedValue); } return formattedQuery; } catch (e) { Logger.log('Error formatting query: $e', type: Logger.ERROR); throw DatabaseException('Error formatting query: $query', e); } } String _formatValue(dynamic value) { if (value == null) return 'NULL'; if (value is DateTime) return "'${value.toIso8601String()}'"; if (value is bool) return value ? '1' : '0'; if (value is List) return value.map(_formatValue).join(', '); return value.toString(); } @override Future execute( String query, [ Map bindings = const {}, ]) async { final startTime = DateTime.now(); try { final formattedQuery = _formatQuery(query, bindings); final result = await _connection.execute(query, bindings); final duration = DateTime.now().difference(startTime); _monitor.recordQuery(_connectionId, formattedQuery, duration); return result; } catch (e) { final duration = DateTime.now().difference(startTime); _monitor.recordQuery( _connectionId, 'Failed: ${_formatQuery(query, bindings)} - Error: $e', duration, ); rethrow; } } @override Future>> select( String query, [ Map bindings = const {}, ]) async { final startTime = DateTime.now(); try { final formattedQuery = _formatQuery(query, bindings); final result = await _connection.select(query, bindings); final duration = DateTime.now().difference(startTime); _monitor.recordQuery(_connectionId, formattedQuery, duration); return result; } catch (e) { final duration = DateTime.now().difference(startTime); _monitor.recordQuery( _connectionId, 'Failed: ${_formatQuery(query, bindings)} - Error: $e', duration, ); rethrow; } } @override Future insert( String query, [ Map bindings = const {}, ]) async { final startTime = DateTime.now(); try { final formattedQuery = _formatQuery(query, bindings); final result = await _connection.insert(query, bindings); final duration = DateTime.now().difference(startTime); _monitor.recordQuery(_connectionId, formattedQuery, duration); return result; } catch (e) { final duration = DateTime.now().difference(startTime); _monitor.recordQuery( _connectionId, 'Failed: ${_formatQuery(query, bindings)} - Error: $e', duration, ); rethrow; } } @override Future transaction(Future Function() action) async => _connection.transaction(action); } ================================================ FILE: lib/src/database/_database_utils/_db_config.dart ================================================ class DBConfig { final String driver; final String host; final int port; final String username; final String password; final String database; final String collation; final bool sslMode; final bool openInMemorySQLite; final String? filePath; final String? schema; String? timezone; final bool pool; int poolSize; DBConfig({ required this.driver, this.host = 'mysql', this.port = 3306, this.username = 'root', this.password = '', this.database = 'vania', this.timezone, this.filePath, this.sslMode = false, this.openInMemorySQLite = false, this.collation = 'utf8mb4_general_ci', this.schema, this.pool = false, this.poolSize = 2, }); } ================================================ FILE: lib/src/database/_database_utils/_paginated_result.dart ================================================ part of '../../contract/database/query_builder/query_builder.dart'; class PaginatedResult { final List> data; final int currentPage; final int perPage; final int total; final int lastPage; final bool isFirst; final bool isLast; final bool hasMore; PaginatedResult({ required this.data, required this.currentPage, required this.perPage, required this.total, required this.lastPage, required this.isFirst, required this.isLast, required this.hasMore, }); Map toMap() => { 'data': data, 'current_page': currentPage, 'per_page': perPage, 'total': total, 'last_page': lastPage, 'is_first': isFirst, 'is_last': isLast, 'has_more': hasMore, }; @override String toString() { return 'PaginatedResult(currentPage: $currentPage, perPage: $perPage, total: $total, lastPage: $lastPage, data: $data)'; } } ================================================ FILE: lib/src/database/_database_utils/_raw_expression.dart ================================================ part of '../../contract/database/query_builder/query_builder.dart'; class RawExpression { final String expression; RawExpression(this.expression); @override String toString() => expression; } ================================================ FILE: lib/src/database/_database_utils/_singularize.dart ================================================ class Singularize { static String make(String tableName) { if (tableName.isEmpty) return tableName; final name = tableName.toLowerCase(); const irregularPlurals = { 'children': 'child', 'people': 'person', 'men': 'man', 'women': 'woman', 'feet': 'foot', 'teeth': 'tooth', 'geese': 'goose', 'mice': 'mouse', 'oxen': 'ox', 'sheep': 'sheep', 'deer': 'deer', 'fish': 'fish', 'series': 'series', 'species': 'species', 'data': 'datum', 'media': 'medium', 'criteria': 'criterion', 'phenomena': 'phenomenon', 'alumni': 'alumnus', 'cacti': 'cactus', 'foci': 'focus', 'fungi': 'fungus', 'nuclei': 'nucleus', 'radii': 'radius', 'stimuli': 'stimulus', 'syllabi': 'syllabus', 'analyses': 'analysis', 'bases': 'basis', 'crises': 'crisis', 'diagnoses': 'diagnosis', 'hypotheses': 'hypothesis', 'oases': 'oasis', 'parentheses': 'parenthesis', 'synopses': 'synopsis', 'theses': 'thesis', }; if (irregularPlurals.containsKey(name)) { return _preserveCase(tableName, irregularPlurals[name]!); } if (name.endsWith('ies') && name.length > 3) { return _preserveCase(tableName, '${name.substring(0, name.length - 3)}y'); } if (name.endsWith('ves') && name.length > 3) { final stem = name.substring(0, name.length - 3); if (stem.endsWith('l') || stem.endsWith('r')) { return _preserveCase(tableName, '${stem}f'); } else { return _preserveCase(tableName, '${stem}fe'); } } if (name.endsWith('ses') && name.length > 3) { return _preserveCase(tableName, name.substring(0, name.length - 2)); } if ((name.endsWith('ches') || name.endsWith('shes') || name.endsWith('xes') || name.endsWith('zes')) && name.length > 3) { return _preserveCase(tableName, name.substring(0, name.length - 2)); } if (name.endsWith('oes') && name.length > 3) { return _preserveCase(tableName, name.substring(0, name.length - 2)); } if (name.endsWith('i') && name.length > 2) { final stem = name.substring(0, name.length - 1); if (stem.endsWith('cact') || stem.endsWith('fung') || stem.endsWith('nucle') || stem.endsWith('radi') || stem.endsWith('stimul') || stem.endsWith('syllab')) { return _preserveCase(tableName, '${stem}us'); } } if (name.endsWith('a') && name.length > 2) { final stem = name.substring(0, name.length - 1); if (stem.endsWith('dat')) { return _preserveCase(tableName, '${stem}um'); } else if (stem.endsWith('criteri') || stem.endsWith('phenomen')) { return _preserveCase(tableName, '${stem}on'); } } if (name.endsWith('s') && name.length > 1 && !name.endsWith('ss') && !name.endsWith('us') && !name.endsWith('is')) { return _preserveCase(tableName, name.substring(0, name.length - 1)); } return tableName; } static String pluralize(String word) { if (word.isEmpty) return word; final name = word.toLowerCase(); const irregularSingulars = { 'child': 'children', 'person': 'people', 'man': 'men', 'woman': 'women', 'foot': 'feet', 'tooth': 'teeth', 'goose': 'geese', 'mouse': 'mice', 'ox': 'oxen', 'sheep': 'sheep', 'deer': 'deer', 'fish': 'fish', 'series': 'series', 'species': 'species', 'datum': 'data', 'medium': 'media', 'criterion': 'criteria', 'phenomenon': 'phenomena', 'alumnus': 'alumni', 'cactus': 'cacti', 'focus': 'foci', 'fungus': 'fungi', 'nucleus': 'nuclei', 'radius': 'radii', 'stimulus': 'stimuli', 'syllabus': 'syllabi', 'analysis': 'analyses', 'basis': 'bases', 'crisis': 'crises', 'diagnosis': 'diagnoses', 'hypothesis': 'hypotheses', 'oasis': 'oases', 'parenthesis': 'parentheses', 'synopsis': 'synopses', 'thesis': 'theses', }; if (irregularSingulars.containsKey(name)) { return _preserveCase(word, irregularSingulars[name]!); } if (name.endsWith('y') && name.length > 1) { final precedingChar = name[name.length - 2]; if (!'aeiou'.contains(precedingChar)) { return _preserveCase(word, '${name.substring(0, name.length - 1)}ies'); } } if (name.endsWith('f') && name.length > 1) { return _preserveCase(word, '${name.substring(0, name.length - 1)}ves'); } if (name.endsWith('fe') && name.length > 2) { return _preserveCase(word, '${name.substring(0, name.length - 2)}ves'); } if (name.endsWith('s') || name.endsWith('ss') || name.endsWith('sh') || name.endsWith('ch') || name.endsWith('x') || name.endsWith('z')) { return _preserveCase(word, '${name}es'); } if (name.endsWith('o') && name.length > 1) { final precedingChar = name[name.length - 2]; if (!'aeiou'.contains(precedingChar)) { return _preserveCase(word, '${name}es'); } } if (name.endsWith('us') && name.length > 2) { final stem = name.substring(0, name.length - 2); if (stem.endsWith('cact') || stem.endsWith('foc') || stem.endsWith('fung') || stem.endsWith('nucle') || stem.endsWith('radi') || stem.endsWith('stimul') || stem.endsWith('syllab')) { return _preserveCase(word, '${stem}i'); } } if (name.endsWith('um') && name.length > 2) { final stem = name.substring(0, name.length - 2); if (stem.endsWith('dat')) { return _preserveCase(word, '${stem}a'); } } if (name.endsWith('on') && name.length > 2) { final stem = name.substring(0, name.length - 2); if (stem.endsWith('criteri') || stem.endsWith('phenomen')) { return _preserveCase(word, '${stem}a'); } } return _preserveCase(word, '${name}s'); } static String _preserveCase(String original, String converted) { if (original.isEmpty || converted.isEmpty) return converted; if (original == original.toUpperCase()) { return converted.toUpperCase(); } if (original[0] == original[0].toUpperCase()) { return '${converted[0].toUpperCase()}${converted.substring(1)}'; } return converted; } static bool isPlural(String word) { if (word.isEmpty) return false; final name = word.toLowerCase(); const irregularPlurals = { 'children', 'people', 'men', 'women', 'feet', 'teeth', 'geese', 'mice', 'oxen', 'data', 'media', 'criteria', 'phenomena', 'alumni', 'cacti', 'foci', 'fungi', 'nuclei', 'radii', 'stimuli', 'syllabi', 'analyses', 'bases', 'crises', 'diagnoses', 'hypotheses', 'oases', 'parentheses', 'synopses', 'theses', }; if (irregularPlurals.contains(name)) return true; if (name.endsWith('ies') && name.length > 3) return true; if (name.endsWith('ves') && name.length > 3) return true; if (name.endsWith('ses') && name.length > 3) return true; if ((name.endsWith('ches') || name.endsWith('shes') || name.endsWith('xes') || name.endsWith('zes')) && name.length > 3) { return true; } if (name.endsWith('oes') && name.length > 3) return true; if (name.endsWith('s') && name.length > 1 && !name.endsWith('ss') && !name.endsWith('us') && !name.endsWith('is')) { return true; } return false; } static bool isSingular(String word) { return !isPlural(word); } } ================================================ FILE: lib/src/database/adapters/_mysql_connector.dart ================================================ import 'dart:convert'; import 'package:mysql_client/mysql_client.dart'; import '../../exception/database_exception.dart'; import '../_database_utils/_db_config.dart'; import '../../contract/database/_connectors/_database_connection.dart'; class MySqlConnector implements DatabaseConnection { final DBConfig config; late dynamic _connection; bool _isPool = false; MySqlConnector(this.config); @override Future close() async { await _connection.close(); } @override Future connect() async { try { if (config.pool) { _isPool = true; _connection = MySQLConnectionPool( host: config.host, port: config.port, userName: config.username, password: config.password, databaseName: config.database, collation: config.collation, secure: config.sslMode, maxConnections: config.poolSize, ); } else { _isPool = false; _connection = await MySQLConnection.createConnection( host: config.host, port: config.port, userName: config.username, password: config.password, databaseName: config.database, collation: config.collation, secure: config.sslMode, ); await _connection.connect(); } } catch (e) { throw DatabaseException('Database connection failed', e); } } @override Future execute( String query, [ Map bindings = const {}, ]) async { try { await _connection.execute(query, bindings); return true; } catch (e) { rethrow; } } @override Future transaction(Future Function() action) async { try { if (_isPool) { return await (_connection as MySQLConnectionPool).transactional(( txConn, ) async { final previous = _connection; _connection = txConn; _isPool = false; try { return await action(); } finally { _isPool = true; _connection = previous; } }); } else { return await (_connection as MySQLConnection).transactional(( txConn, ) async { final previous = _connection; _connection = txConn; try { return await action(); } finally { _connection = previous; } }); } } catch (e) { throw DatabaseException('Transaction failed', e); } } @override Future>> select( String query, [ Map bindings = const {}, ]) async { try { final IResultSet results; if (_isPool) { results = await (_connection as MySQLConnectionPool).execute( query, bindings, ); } else { results = await (_connection as MySQLConnection).execute( query, bindings, ); } if (results.rows.isEmpty) { return []; } return results.rows.map((item) => item.assoc()).toList().map((row) { final newRow = Map.from(row); newRow.forEach((key, value) { if (value is List) { try { newRow[key] = utf8.decode(value); } catch (e) { newRow[key] = value; } } }); return newRow; }).toList(); } catch (e) { rethrow; } } @override Future insert( String query, [ Map bindings = const {}, ]) async { try { final IResultSet results; if (_isPool) { results = await (_connection as MySQLConnectionPool).execute( query, bindings, ); } else { results = await (_connection as MySQLConnection).execute( query, bindings, ); } return results.lastInsertID; } catch (e) { rethrow; } } } ================================================ FILE: lib/src/database/adapters/_postgres_connector.dart ================================================ import 'dart:convert'; import 'package:postgres/postgres.dart'; import 'package:vania/src/exception/database_exception.dart'; import '../_database_utils/_db_config.dart'; import '../../contract/database/_connectors/_database_connection.dart'; class PostgresConnector implements DatabaseConnection { final DBConfig config; late dynamic _connection; PostgresConnector(this.config); @override Future close() async { await _connection.close(); } Encoding _getEncoding(String encoding) { switch (encoding.toLowerCase()) { case 'utf8': return Utf8Codec(); case 'ascii': return AsciiCodec(); case 'latin1': return Latin1Codec(); case 'iso-8859-1': return Latin1Codec(); default: return Utf8Codec(allowMalformed: true); } } Future _onOpen(Connection conn, DBConfig configParser) async { await conn.execute("SET client_encoding = '${configParser.collation}'"); if (configParser.schema != null) { await conn.execute("SET search_path TO ${configParser.schema}"); } if (configParser.timezone != null) { await conn.execute("SET timezone TO '${configParser.timezone}'"); } } @override Future connect() async { final endpoint = Endpoint( host: config.host, port: config.port, database: config.database, username: config.username, password: config.password, ); final sslMode = config.sslMode ? SslMode.require : SslMode.disable; try { if (config.pool == true) { _connection = Pool.withEndpoints( [endpoint], settings: PoolSettings( timeZone: config.timezone, maxConnectionCount: config.poolSize, encoding: _getEncoding(config.collation), onOpen: (conn) async { await _onOpen(conn, config); }, sslMode: sslMode, ), ); } else { _connection = await Connection.open( endpoint, settings: ConnectionSettings( timeZone: config.timezone, encoding: _getEncoding(config.collation), sslMode: sslMode, onOpen: (conn) async { await _onOpen(conn, config); }, ), ); } } catch (e) { throw DatabaseException('Database connection failed', e); } } @override Future execute( String query, [ Map bindings = const {}, ]) async { try { final result = await _connection.execute( Sql.named(query.replaceAll(':p', '@p')), parameters: bindings, ); return result.affectedRows > 0; } catch (e) { rethrow; } } @override Future>> select( String query, [ Map bindings = const {}, ]) async { try { final sql = Sql.named(query.replaceAll(':p', '@p')); final result = await _connection.execute(sql, parameters: bindings); final rows = result.map((row) => row.toColumnMap()).toList(); final maps = >[]; if (rows.isNotEmpty) { for (final row in rows) { final map = {}; for (final col in row.entries) { final key = col.key; final value = col.value is UndecodedBytes ? col.value.asString : col.value; map.addAll({key: value}); } maps.add(map); } } return maps; } catch (e) { rethrow; } } @override Future insert( String query, [ Map bindings = const {}, ]) async { try { final result = await _connection.execute( Sql.named(query.replaceAll(':p', '@p')), parameters: bindings, ); return result.affectedRows; } catch (e) { rethrow; } } @override Future transaction(Future Function() action) async { return await _connection.runTx((ctx) async { final prev = _connection; _connection = ctx; try { return await action(); } finally { _connection = prev; } }); } } ================================================ FILE: lib/src/database/adapters/_sqlite_connector.dart ================================================ import 'dart:ffi'; import 'dart:io'; import 'package:path/path.dart'; import 'package:sqlite3/open.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:vania/src/contract/database/_connectors/_database_connection.dart'; import 'package:vania/src/exception/database_exception.dart'; import '../../utils/helper.dart' show env; import '../_database_utils/_db_config.dart'; class SQLiteConnector implements DatabaseConnection { final DBConfig config; late Database _connection; SQLiteConnector(this.config); @override Future close() async { _connection.dispose(); } @override Future connect() async { try { open.overrideFor(OperatingSystem.linux, _openOnLinux); if (config.openInMemorySQLite) { _connection = sqlite3.openInMemory(); } else { _connection = sqlite3.open( config.filePath ?? '${env('APP_NAME', 'Vania')}.db', ); } } catch (e) { throw DatabaseException('Database connection failed', e); } } DynamicLibrary _openOnLinux() { final scriptDir = File(Platform.script.toFilePath()).parent; final libraryNextToScript = File(join(scriptDir.path, 'sqlite3.so')); return DynamicLibrary.open(libraryNextToScript.path); } List _convertBindingsToList( Map bindings, String query, ) { if (bindings.isEmpty) return []; final List result = []; final parameterRegex = RegExp(r':(\w+)'); final matches = parameterRegex.allMatches(query); for (final match in matches) { final paramName = match.group(1)!; if (bindings.containsKey(paramName)) { result.add(bindings[paramName]); } } return result; } String _convertNamedParamsToPositional(String query) { return query.replaceAllMapped(RegExp(r':(\w+)'), (match) => '?'); } @override Future execute( String query, [ Map bindings = const {}, ]) async { try { final positionalQuery = _convertNamedParamsToPositional(query); final params = _convertBindingsToList(bindings, query); final stmt = _connection.prepare(positionalQuery); stmt.execute(params); stmt.dispose(); return true; } catch (e) { rethrow; } } @override Future>> select( String query, [ Map bindings = const {}, ]) async { try { final positionalQuery = _convertNamedParamsToPositional(query); final params = _convertBindingsToList(bindings, query); final stmt = _connection.prepare(positionalQuery); final results = stmt.select(params); final List> rows = []; final columns = results.isEmpty ? [] : results.first.keys.toList(); for (final row in results) { final map = {}; for (var i = 0; i < columns.length; i++) { map[columns[i]] = row[i.toString()]; } rows.add(map); } stmt.dispose(); return rows; } catch (e) { rethrow; } } @override Future insert( String query, [ Map bindings = const {}, ]) async { try { final positionalQuery = _convertNamedParamsToPositional(query); final params = _convertBindingsToList(bindings, query); final stmt = _connection.prepare(positionalQuery); stmt.execute(params); final id = _connection.lastInsertRowId; stmt.dispose(); return id; } catch (e) { rethrow; } } @override Future transaction(Future Function() action) { throw UnimplementedError(); } } ================================================ FILE: lib/src/database/db.dart ================================================ // ignore_for_file: non_constant_identifier_names import '../contract/database/query_builder/query_builder.dart'; import 'query_builder/_query_builder_impl.dart' show QueryBuilderImpl; QueryBuilder get DB => QueryBuilderImpl(); ================================================ FILE: lib/src/database/isolate_db.dart ================================================ import 'dart:isolate'; import '_connection_manager.dart'; import '_database_utils/_db_config.dart'; class IsolateDB { static Future run( Future Function() callback, Map dbConfig, ) async { return await Isolate.run(() async { ConnectionManager().defaultConnection = dbConfig['driver']; final config = DBConfig( driver: dbConfig['driver'] ?? '', host: dbConfig['host'] ?? '', port: dbConfig['port'] ?? 3306, database: dbConfig['database'] ?? '', username: dbConfig['username'] ?? '', password: dbConfig['password'] ?? '', sslMode: dbConfig['sslmode'] ?? false, collation: dbConfig['collation'] ?? 'utf8mb4_general_ci', pool: false, poolSize: 0, filePath: dbConfig['file_path'], openInMemorySQLite: dbConfig['openInMemorySQLite'] ?? false, ); try { await ConnectionManager().connect(config, dbConfig['driver']); return await callback(); } finally { await ConnectionManager().connection(dbConfig['driver'])?.close(); } }); } } ================================================ FILE: lib/src/database/migration/adapters/grammar/mysql_grammar.dart ================================================ import 'sql_grammar.dart'; class MySqlGrammar extends BaseGrammar { @override String get identifierQuote => '`'; @override Map get dataTypeMappings => {}; @override Map get keywordReplacements => {}; @override Map get regexTransformations => {}; @override String convertQuery(String query) { return _cleanupQuery(query); } String _cleanupQuery(String query) { return query .replaceAll(RegExp(r'\s+'), ' ') .replaceAll(',,', ',') .replaceAll(RegExp(r',\s?\)'), ')') .trim(); } } ================================================ FILE: lib/src/database/migration/adapters/grammar/postgresql_grammar.dart ================================================ import 'sql_grammar.dart'; /// PostgreSQL-specific SQL grammar (Single Responsibility Principle) class PostgreSqlGrammar extends BaseGrammar { @override String get identifierQuote => '"'; @override Map get dataTypeMappings => { // Integer types r'BIGINT\(\d+\)': 'BIGINT', r'MEDIUMINT\(\d+\)': 'INTEGER', r'SMALLINT\(\d+\)': 'SMALLINT', r'TINYINT\(\d+\)': 'SMALLINT', // Binary types r'BINARY\(\d+\)': 'BYTEA', r'VARBINARY\(\d+\)': 'BYTEA', r'BIT\(\d+\)': 'BOOLEAN', // Date/time types r'DATETIME\(\d+\)': 'TIMESTAMP', r'TIME\(\d+\)': 'TIME', // Floating point types r'DOUBLE\(\d+\)': 'DOUBLE PRECISION', // BLOB types 'TINYBLOB': 'BYTEA', 'BLOB': 'BYTEA', 'MEDIUMBLOB': 'BYTEA', 'LONGBLOB': 'BYTEA', 'VARBYTEA': 'BYTEA', 'MEDIUMBYTEA': 'BYTEA', 'LONGBYTEA': 'BYTEA', // Text types 'TINYTEXT': 'TEXT', 'MEDIUMTEXT': 'TEXT', r'LONGTEXT\(\d+\)': 'TEXT', // Geometry types 'LINESTRING': 'LINE', }; @override Map get keywordReplacements => { 'UNSIGNED': '', 'ZEROFILL': '', 'AUTO_INCREMENT': '', r"COLLATE '[\w\d_-]+'": '', r"ENGINE\s*=\s*\w+": '', r"COMMENT\s+'[^']*'": '', }; @override Map get regexTransformations => { // Auto-increment primary key transformation - handle table.id() pattern r'[`"](\w+)[`"]\s+BIGINT(?:\(\d+\))?\s+(?:UNSIGNED\s+)?NOT\s+NULL\s+AUTO_INCREMENT': (match) => '"${match[1]}" SERIAL NOT NULL PRIMARY KEY', r'\s+ON\s+UPDATE\s+CURRENT_TIMESTAMP': (match) => '', r'ON\s+UPDATE\s+CURRENT_TIMESTAMP': (match) => '', // Handle `BIGINT` NOT NULL without AUTO_INCREMENT but with separate PRIMARY KEY r'[`"](\w+)[`"]\s+BIGINT(?:\(\d+\))?\s+(?:UNSIGNED\s+)?NOT\s+NULL(?!\s+AUTO_INCREMENT)': (match) => '"${match[1]}" BIGINT NOT NULL', // Remove primary key declarations when SERIAL is used r',\s*PRIMARY KEY \([`"][^`"]+[`"]\)': (match) => '', r'PRIMARY KEY \([`"][^`"]+[`"]\)\s*,?': (match) => '', // Remove INDEX declarations - more comprehensive patterns r',\s*INDEX\s+[`"][^`"]*[`"]\s*\([^)]*\)': (match) => '', r'INDEX\s+[`"][^`"]*[`"]\s*\([^)]*\)\s*,': (match) => '', r'INDEX\s+[`"][^`"]*[`"]\s*\([^)]*\)': (match) => '', // Remove CONSTRAINT UNIQUE (will be handled by adapter) r',\s*CONSTRAINT\s+[`"][^`"]*[`"]\s+UNIQUE\s*\([^)]*\)': (match) => '', r'CONSTRAINT\s+[`"][^`"]*[`"]\s+UNIQUE\s*\([^)]*\)\s*,': (match) => '', r'CONSTRAINT\s+[`"][^`"]*[`"]\s+UNIQUE\s*\([^)]*\)': (match) => '', // Integer type with length r'(^|\s|,)INT\((\d+)\)': (match) => '${match[1]}INTEGER', r'(^|\s|,)INTEGER\((\d+)\)': (match) => '${match[1]}INTEGER', // VARCHAR with length preservation r'VARCHAR\((\d+)\)': (match) => 'VARCHAR(${match[1]})', r'VARCHARACTER\((\d+)\)': (match) => 'CHARACTER(${match[1]})', // FLOAT conversion r'FLOAT\((\d+)\)': (match) => 'REAL', // ENUM to VARCHAR conversion r"ENUM\((?:'[^']*'(?:\s*,\s*'[^']*')*)\)": (match) => 'VARCHAR', // Clean up empty spaces and commas that result from removals r',\s*,+': (match) => ',', r'^\s*,': (match) => '', r',\s*\)': (match) => ')', r'\(\s*,': (match) => '(', }; @override String convertQuery(String query) { String result = super.convertQuery(query); // Additional PostgreSQL-specific cleanup result = _postgresqlSpecificCleanup(result); return result; } /// PostgreSQL-specific cleanup operations String _postgresqlSpecificCleanup(String query) { return query // Remove any remaining double commas .replaceAll(RegExp(r',\s*,+'), ',') // Remove leading commas .replaceAll(RegExp(r'^\s*,'), '') // Remove trailing commas before closing parenthesis .replaceAll(RegExp(r',\s*\)'), ')') // Remove extra spaces .replaceAll(RegExp(r'\s+'), ' ') // Clean up any remaining issues .trim(); } } ================================================ FILE: lib/src/database/migration/adapters/grammar/sql_grammar.dart ================================================ abstract class SqlGrammar { String convertQuery(String query); String get identifierQuote; Map get dataTypeMappings; Map get keywordReplacements; Map get regexTransformations; } abstract class BaseGrammar implements SqlGrammar { @override String convertQuery(String query) { String result = query; for (final entry in regexTransformations.entries) { result = result.replaceAllMapped( RegExp(entry.key, caseSensitive: false), entry.value, ); } for (final entry in keywordReplacements.entries) { result = result.replaceAll( RegExp(entry.key, caseSensitive: false), entry.value, ); } for (final entry in dataTypeMappings.entries) { result = result.replaceAll( RegExp(entry.key, caseSensitive: false), entry.value, ); } result = result.replaceAll('`', identifierQuote); result = _cleanupQuery(result); return result; } String _cleanupQuery(String query) { return query .replaceAll(RegExp(r'\s+'), ' ') .replaceAll(',,', ',') .replaceAll(RegExp(r',\s?\)'), ')') .trim(); } } ================================================ FILE: lib/src/database/migration/adapters/grammar/sqlite_grammar.dart ================================================ import 'sql_grammar.dart'; class SqliteGrammar extends BaseGrammar { @override String get identifierQuote => '"'; @override Map get dataTypeMappings => { // Integer types - SQLite uses INTEGER for all integer types r'BIGINT\(\d+\)': 'INTEGER', r'MEDIUMINT\(\d+\)': 'INTEGER', r'SMALLINT\(\d+\)': 'INTEGER', r'TINYINT\(\d+\)': 'INTEGER', // Binary and bit types r'BINARY\(\d+\)': 'BLOB', r'VARBINARY\(\d+\)': 'BLOB', r'BIT\(\d+\)': 'INTEGER', // String types - SQLite uses TEXT for all string types r'VARCHAR\(\d+\)': 'TEXT', r'CHAR\(\d+\)': 'TEXT', // Date/time types - SQLite stores as TEXT or INTEGER 'DATETIME': 'TEXT', 'TIMESTAMP': 'TEXT', 'DATE': 'TEXT', 'TIME': 'TEXT', 'YEAR': 'INTEGER', // BLOB types 'TINYBLOB': 'BLOB', 'BLOB': 'BLOB', 'MEDIUMBLOB': 'BLOB', 'LONGBLOB': 'BLOB', // Text types 'TINYTEXT': 'TEXT', 'MEDIUMTEXT': 'TEXT', 'LONGTEXT': 'TEXT', // JSON - SQLite stores as TEXT 'JSON': 'TEXT', }; @override Map get keywordReplacements => { 'UNSIGNED': '', 'ZEROFILL': '', 'AUTO_INCREMENT': '', r"COLLATE\s+'[^']+'": '', r"ENGINE\s*=\s*\w+": '', r"COLLATE\s*=\s*'[^']+'": '', }; @override Map get regexTransformations => { // Auto-increment primary key transformation r'`(\w+)`\s+BIGINT\(\d+\)\s+UNSIGNED\s+NOT\s+NULL\s+AUTO_INCREMENT': (match) => '"${match[1]}" INTEGER PRIMARY KEY AUTOINCREMENT', // Remove primary key declarations (handled by AUTOINCREMENT) r'PRIMARY KEY \(`.*?`\) USING BTREE': (match) => '', r'PRIMARY KEY \(`.*?`\)': (match) => '', // Integer type with length r'(^|\s|,)INT\((\d+)\)': (match) => '${match[1]}INTEGER', r'(^|\s|,)INTEGER\((\d+)\)': (match) => '${match[1]}INTEGER', // Floating point types r'FLOAT\((\d+),(\d+)\)': (match) => 'REAL', r'DOUBLE\((\d+),(\d+)\)': (match) => 'REAL', r'DECIMAL\((\d+),(\d+)\)': (match) => 'REAL', // ENUM - SQLite doesn't have ENUM, use TEXT with CHECK constraint r'ENUM\(([^)]+)\)': (match) => 'TEXT CHECK (${_extractColumnName()} IN (${match[1]}))', }; String _extractColumnName() { return 'column_name'; } @override String convertQuery(String query) { String result = super.convertQuery(query); if (result.contains('TEXT CHECK (column_name IN')) { result = _handleEnumConstraints(query, result); } return result; } String _handleEnumConstraints(String originalQuery, String convertedQuery) { final enumMatch = RegExp( r'"?(\w+)"?\s+ENUM\(([^)]+)\)', caseSensitive: false, ).firstMatch(originalQuery); if (enumMatch != null) { final columnName = enumMatch.group(1); final enumValues = enumMatch.group(2); return convertedQuery.replaceAll( 'TEXT CHECK (column_name IN ($enumValues))', 'TEXT CHECK ("$columnName" IN ($enumValues))', ); } return convertedQuery; } } ================================================ FILE: lib/src/database/migration/adapters/mysql_adapter.dart ================================================ import '../contracts/database_adapter_interface.dart'; import 'grammar/mysql_grammar.dart'; class MySqlAdapter implements DatabaseAdapterInterface { late final MySqlGrammar _grammar; MySqlAdapter() { _grammar = MySqlGrammar(); } @override String get driverName => 'mysql'; @override String adaptQuery(String query) { return _grammar.convertQuery(query); } @override String getMigrationsTableSql() { return ''' CREATE TABLE IF NOT EXISTS `migrations` ( `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `migration` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_unicode_ci', `batch` INT(10) UNSIGNED NOT NULL DEFAULT 1, PRIMARY KEY (`id`) USING BTREE ) COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB ; '''; } @override bool supports(String driver) { return driver.toLowerCase() == 'mysql'; } @override String escapeIdentifier(String identifier) { return '`$identifier`'; } @override String formatValue(dynamic value) { if (value == null) return 'NULL'; if (value is String) return "'${value.replaceAll("'", "''")}'"; if (value is num) return value.toString(); if (value is bool) return value ? '1' : '0'; return "'$value'"; } } ================================================ FILE: lib/src/database/migration/adapters/postgresql_adapter.dart ================================================ import '../contracts/database_adapter_interface.dart'; import 'grammar/postgresql_grammar.dart'; class PostgreSqlAdapter implements DatabaseAdapterInterface { late final PostgreSqlGrammar _grammar; final List _extractedIndexes = []; String? _currentTableName; PostgreSqlAdapter() { _grammar = PostgreSqlGrammar(); } @override String get driverName => 'pgsql'; String _cleanedQuery = ''; @override String adaptQuery(String query) { List statements = adaptQueryToStatements(query); return statements.join(';\n'); } List adaptQueryToStatements(String query) { _extractTableNameAndIndexes(query); String result = _grammar.convertQuery(_cleanedQuery); List statements = [result]; if (_extractedIndexes.isNotEmpty && _currentTableName != null) { List indexStatements = _generateIndexStatements(); statements.addAll(indexStatements); } return statements; } Future executeStatements( String query, Future Function(List) executor, ) async { List statements = adaptQueryToStatements(query); await executor(statements); } void _extractTableNameAndIndexes(String query) { _extractedIndexes.clear(); _currentTableName = null; final tableNameRegex = RegExp( r'CREATE TABLE (?:IF NOT EXISTS )?[`"]?([^`"]+)[`"]?\s*\(', caseSensitive: false, ); final tableMatch = tableNameRegex.firstMatch(query); if (tableMatch != null) { _currentTableName = tableMatch.group(1); } final indexRegex = RegExp( r'((?:SPATIAL|FULLTEXT|UNIQUE)\s+)?INDEX\s+[`"]([^`"]+)[`"]\s*\(([^)]+)\)', caseSensitive: false, ); final rawIndexes = indexRegex.allMatches(query).toList(); for (final m in rawIndexes) { final typeKey = m.group(1)?.trim().toUpperCase() ?? ''; final name = m.group(2)!; final cols = m.group(3)!; final cleanCols = cols .replaceAll(RegExp(r'[`"]'), '') .replaceAll(RegExp(r'\s+'), ' ') .trim(); _extractedIndexes.add('$name:$cleanCols:$typeKey'); } query = query.replaceAll(indexRegex, ''); query = query.replaceAll(RegExp(r',\s*\)'), ')'); final constraintRegex = RegExp( r'CONSTRAINT\s+[`"]([^`"]+)[`"]\s+UNIQUE\s*\(([^)]+)\)', caseSensitive: false, ); for (final m in constraintRegex.allMatches(query)) { final name = m.group(1)!; final cols = m.group(2)!; final cleanCols = cols .replaceAll(RegExp(r'[`"]'), '') .replaceAll(RegExp(r'\s+'), ' ') .trim(); _extractedIndexes.add('$name:$cleanCols:UNIQUE'); } _cleanedQuery = query; } List _generateIndexStatements() { final statements = []; for (final info in _extractedIndexes) { final parts = info.split(':'); final name = parts[0]; final cols = parts[1]; final typeKey = parts.length > 2 ? parts[2] : ''; final colList = cols .split(',') .map((c) => '"${c.trim()}"') .toList() .join(', '); final prefix = typeKey.isNotEmpty ? '$typeKey ' : ''; final stmt = 'CREATE ${prefix}INDEX IF NOT EXISTS "$name" ' 'ON "$_currentTableName" ($colList)'; statements.add(stmt); } return statements; } @override String getMigrationsTableSql() { return ''' CREATE TABLE IF NOT EXISTS "migrations" ( "id" SERIAL NOT NULL PRIMARY KEY, "migration" VARCHAR(255) NOT NULL, "batch" INTEGER NOT NULL DEFAULT 1 ); '''; } @override bool supports(String driver) { final normalizedDriver = driver.toLowerCase(); return normalizedDriver == 'pgsql' || normalizedDriver == 'postgresql' || normalizedDriver == 'postgres'; } @override String escapeIdentifier(String identifier) { return '"$identifier"'; } @override String formatValue(dynamic value) { if (value == null) return 'NULL'; if (value is String) return "'${value.replaceAll("'", "''")}'"; if (value is num) return value.toString(); if (value is bool) return value ? 'TRUE' : 'FALSE'; return "'$value'"; } } ================================================ FILE: lib/src/database/migration/adapters/sqlite_adapter.dart ================================================ import '../contracts/database_adapter_interface.dart'; import 'grammar/sqlite_grammar.dart'; class SqliteAdapter implements DatabaseAdapterInterface { late final SqliteGrammar _grammar; SqliteAdapter() { _grammar = SqliteGrammar(); } @override String get driverName => 'sqlite'; @override String adaptQuery(String query) { return _grammar.convertQuery(query); } @override String getMigrationsTableSql() { return ''' CREATE TABLE IF NOT EXISTS "migrations" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "migration" TEXT NOT NULL, "batch" INTEGER NOT NULL DEFAULT 1 ); '''; } @override bool supports(String driver) { final normalizedDriver = driver.toLowerCase(); return normalizedDriver == 'sqlite' || normalizedDriver == 'sqlite3'; } @override String escapeIdentifier(String identifier) { return '"$identifier"'; } @override String formatValue(dynamic value) { if (value == null) return 'NULL'; if (value is String) return "'${value.replaceAll("'", "''")}'"; if (value is num) return value.toString(); if (value is bool) return value ? '1' : '0'; return "'$value'"; } } ================================================ FILE: lib/src/database/migration/builders/column_definition.dart ================================================ import '../../../enum/column_index.dart'; import 'schema.dart'; class ColumnDefinition { final Schema _schema; final String _name; final String _type; bool _nullable = true; dynamic _length; bool _unsigned = false; bool _zeroFill = false; dynamic _defaultValue; String? _comment; String? _collation; String? _charset; String? _expression; String? _virtuality; bool _increment = false; bool _unique = false; String? _uniqueConstraintName; String? _indexName; ColumnIndex _indexType = ColumnIndex.indexKey; String? _foreignTable; String? _foreignColumn; String? _onUpdate; String? _onDelete; bool _isFinalized = false; ColumnDefinition(this._schema, this._name, this._type) { _schema.registerColumnDefinition(this); } /// Set column as NOT NULL ColumnDefinition notNull() { _nullable = false; return this; } /// Set column as NULLABLE ColumnDefinition nullable() { _nullable = true; return this; } /// Set column length/size ColumnDefinition length(int length) { _length = length; return this; } /// Set column as UNSIGNED (for numeric types) ColumnDefinition unsigned() { _unsigned = true; return this; } /// Set column as ZEROFILL ColumnDefinition zeroFill() { _zeroFill = true; return this; } /// Set default value ColumnDefinition defaultTo(dynamic value) { _defaultValue = value; return this; } /// Set default to CURRENT_TIMESTAMP ColumnDefinition defaultToCurrent() { _defaultValue = 'CURRENT_TIMESTAMP'; return this; } /// Set column comment ColumnDefinition comment(String comment) { _comment = comment; return this; } /// Set column collation ColumnDefinition collate(String collation) { _collation = collation; return this; } /// Set column charset - NOW PROPERLY USED! ColumnDefinition charset(String charset) { _charset = charset; return this; } /// Set column as AUTO_INCREMENT ColumnDefinition autoIncrement() { _increment = true; return this; } /// Set column as UNIQUE ColumnDefinition unique([String? constraintName]) { if (constraintName != null) { _unique = false; _uniqueConstraintName = constraintName; _schema.addCompositeUniqueConstraint(constraintName, [_name]); } else { _unique = true; _uniqueConstraintName = null; } return this; } /// Add index to column - NOW PROPERLY USED! ColumnDefinition index([ String? indexName, ColumnIndex type = ColumnIndex.indexKey, ]) { _indexName = indexName ?? 'idx_${_schema.tableName}_$_name'; _indexType = type; _schema.addCompositeIndex(_indexName!, _name, _indexType); return this; } ColumnDefinition foreignKey( String referencesTable, String referencesColumn, { String onUpdate = 'CASCADE', String onDelete = 'CASCADE', }) { _foreignTable = referencesTable; _foreignColumn = referencesColumn; _onUpdate = onUpdate; _onDelete = onDelete; _schema.foreign( _name, _foreignTable!, _foreignColumn!, onUpdate: _onUpdate ?? 'CASCADE', onDelete: _onDelete ?? 'CASCADE', ); return this; } ColumnDefinition generated( String expression, { String virtuality = 'VIRTUAL', }) { _expression = expression; _virtuality = virtuality; return this; } void finalize() { if (!_isFinalized) { _isFinalized = true; _addColumnToSchema(); } } void _addColumnToSchema() { String columnType = _type; if (_length != null) { columnType = '$_type($_length)'; } String? finalComment = _comment; if (_charset != null) { String charsetInfo = 'charset: $_charset'; finalComment = _comment != null ? '$_comment ($charsetInfo)' : charsetInfo; } bool shouldBeUnique = _unique && _uniqueConstraintName == null; _schema.addColumn( _name, columnType, nullable: _nullable, unsigned: _unsigned, zeroFill: _zeroFill, defaultValue: _defaultValue, comment: finalComment, collation: _collation, expression: _expression, virtuality: _virtuality, increment: _increment, unique: shouldBeUnique, ); } } ================================================ FILE: lib/src/database/migration/builders/column_types.dart ================================================ import 'column_definition.dart'; import 'schema.dart'; export 'column_definition.dart'; export 'table_definition.dart'; extension ColumnTypes on Schema { /// Add an auto-incrementing primary key column ColumnDefinition id([String name = 'id']) { final definition = ColumnDefinition( this, name, 'BIGINT', ).length(20).unsigned().autoIncrement().notNull(); primary(name); return definition; } /// Add a big auto-incrementing column ColumnDefinition bigIncrements(String name) { final definition = ColumnDefinition( this, name, 'BIGINT', ).length(20).unsigned().autoIncrement().notNull(); return definition; } /// Create an integer column ColumnDefinition integer(String name) { return ColumnDefinition(this, name, 'INT').length(10); } /// Create a tiny integer column ColumnDefinition tinyInt(String name) { return ColumnDefinition(this, name, 'TINYINT').length(1); } /// Create a small integer column ColumnDefinition smallInt(String name) { return ColumnDefinition(this, name, 'SMALLINT').length(5); } /// Create a medium integer column ColumnDefinition mediumInt(String name) { return ColumnDefinition(this, name, 'MEDIUMINT').length(8); } /// Create a big integer column ColumnDefinition bigInt(String name) { return ColumnDefinition(this, name, 'BIGINT').length(20); } /// Create a bit column ColumnDefinition bit(String name) { return ColumnDefinition(this, name, 'BIT').length(1); } /// Create a float column ColumnDefinition float(String name, {int? precision, int? scale}) { String type = 'FLOAT'; if (precision != null && scale != null) { type = 'FLOAT($precision,$scale)'; } return ColumnDefinition(this, name, type); } /// Create a double column ColumnDefinition double(String name, {int? precision, int? scale}) { String type = 'DOUBLE'; if (precision != null && scale != null) { type = 'DOUBLE($precision,$scale)'; } return ColumnDefinition(this, name, type); } /// Create a decimal column ColumnDefinition decimal(String name, {int? precision, int? scale}) { String type = 'DECIMAL'; if (precision != null && scale != null) { type = 'DECIMAL($precision,$scale)'; } return ColumnDefinition(this, name, type); } /// Create a string/varchar column ColumnDefinition string(String name) { return ColumnDefinition(this, name, 'VARCHAR').length(255); } /// Create a char column ColumnDefinition char(String name) { return ColumnDefinition(this, name, 'CHAR').length(50); } /// Create a tiny text column ColumnDefinition tinyText(String name) { return ColumnDefinition(this, name, 'TINYTEXT'); } /// Create a text column ColumnDefinition text(String name) { return ColumnDefinition(this, name, 'TEXT'); } /// Create a medium text column ColumnDefinition mediumText(String name) { return ColumnDefinition(this, name, 'MEDIUMTEXT'); } /// Create a long text column ColumnDefinition longText(String name) { return ColumnDefinition(this, name, 'LONGTEXT'); } /// Create a JSON column ColumnDefinition json(String name) { return ColumnDefinition(this, name, 'JSON'); } /// Create a UUID column ColumnDefinition uuid(String name) { return ColumnDefinition(this, name, 'CHAR').length(36); } /// Create a binary column ColumnDefinition binary(String name) { return ColumnDefinition(this, name, 'BINARY').length(50); } /// Create a variable binary column ColumnDefinition varBinary(String name) { return ColumnDefinition(this, name, 'VARBINARY').length(50); } /// Create a tiny blob column ColumnDefinition tinyBlob(String name) { return ColumnDefinition(this, name, 'TINYBLOB'); } /// Create a blob column ColumnDefinition blob(String name) { return ColumnDefinition(this, name, 'BLOB'); } /// Create a medium blob column ColumnDefinition mediumBlob(String name) { return ColumnDefinition(this, name, 'MEDIUMBLOB'); } /// Create a long blob column ColumnDefinition longBlob(String name) { return ColumnDefinition(this, name, 'LONGBLOB'); } /// Create a date column ColumnDefinition date(String name) { return ColumnDefinition(this, name, 'DATE'); } /// Create a time column ColumnDefinition time(String name) { return ColumnDefinition(this, name, 'TIME'); } /// Create a year column ColumnDefinition year(String name) { return ColumnDefinition(this, name, 'YEAR'); } /// Create a datetime column ColumnDefinition dateTime(String name) { return ColumnDefinition(this, name, 'DATETIME'); } /// Create a timestamp column ColumnDefinition timeStamp(String name) { return ColumnDefinition(this, name, 'TIMESTAMP'); } /// Create standard timestamps (created_at, updated_at) void timeStamps() { timeStamp('created_at').nullable(); timeStamp('updated_at').nullable(); } /// Create soft delete timestamp ColumnDefinition softDeletes([String name = 'deleted_at']) { return timeStamp(name).nullable(); } /// Create a point geometry column ColumnDefinition point(String name) { return ColumnDefinition(this, name, 'POINT'); } /// Create a line string geometry column ColumnDefinition lineString(String name) { return ColumnDefinition(this, name, 'LINESTRING'); } /// Create a polygon geometry column ColumnDefinition polygon(String name) { return ColumnDefinition(this, name, 'POLYGON'); } /// Create a geometry column ColumnDefinition geometry(String name) { return ColumnDefinition(this, name, 'GEOMETRY'); } /// Create a multi-point geometry column ColumnDefinition multiPoint(String name) { return ColumnDefinition(this, name, 'MULTIPOINT'); } /// Create a multi-line string geometry column ColumnDefinition multiLineString(String name) { return ColumnDefinition(this, name, 'MULTILINESTRING'); } /// Create a multi-polygon geometry column ColumnDefinition multiPolygon(String name) { return ColumnDefinition(this, name, 'MULTIPOLYGON'); } /// Create a geometry collection column ColumnDefinition geometryCollection(String name) { return ColumnDefinition(this, name, 'GEOMETRYCOLLECTION'); } /// Create an enum column ColumnDefinition enumType(String name, List values) { final enumValuesString = values.map((value) => "'$value'").join(', '); return ColumnDefinition(this, name, 'ENUM($enumValuesString)'); } /// Create a set column ColumnDefinition setType(String name, List values) { final setValuesString = values.map((value) => "'$value'").join(', '); return ColumnDefinition(this, name, 'SET($setValuesString)'); } /// Create a boolean column ColumnDefinition boolean(String name) { return ColumnDefinition(this, name, 'TINYINT').length(1); } } ================================================ FILE: lib/src/database/migration/builders/schema.dart ================================================ import '../../../enum/column_index.dart'; import '../contracts/schema_interface.dart'; class Schema implements SchemaInterface { final List _queries = []; final List _foreignKeys = []; final List _indexes = []; final List _columnDefinitions = []; final Map> _compositeUniqueConstraints = {}; final Map> _compositeIndexes = {}; String _primaryField = ''; String _primaryAlgorithm = ''; String _tableName = ''; void setTableName(String tableName) { _tableName = tableName; } void registerColumnDefinition(dynamic columnDefinition) { _columnDefinitions.add(columnDefinition); } void _finalizeColumnDefinitions() { for (final columnDef in _columnDefinitions) { columnDef.finalize(); } _columnDefinitions.clear(); _addCompositeUniqueConstraints(); _addCompositeIndexes(); } void _addCompositeUniqueConstraints() { _compositeUniqueConstraints.forEach((constraintName, columns) { final constraint = 'CONSTRAINT `$constraintName` UNIQUE (${columns.map((col) => '`$col`').join(', ')})'; _indexes.add(constraint); }); } void _addCompositeIndexes() { _compositeIndexes.forEach((indexName, indexData) { List columns = indexData['columns']; ColumnIndex type = indexData['type']; if (type == ColumnIndex.indexKey) { _indexes.add( 'INDEX `$indexName` (${columns.map((e) => "`$e`").join(', ')})', ); } else { _indexes.add( '${type.name} INDEX `$indexName` (${columns.map((e) => "`$e`").join(', ')})', ); } }); } @override void addColumn( String name, String type, { bool nullable = false, dynamic length, bool unsigned = false, bool zeroFill = false, dynamic defaultValue, String? comment, String? collation, String? expression, String? virtuality, bool increment = false, bool unique = false, }) { final columnDefinition = StringBuffer(' `$name` $type'); if (length != null) { columnDefinition.write('($length)'); } if (unsigned) { columnDefinition.write(' UNSIGNED'); } if (zeroFill) { columnDefinition.write(' ZEROFILL'); } String nullableStr = nullable ? 'NULL' : 'NOT NULL'; columnDefinition.write( ' ' * (20 - columnDefinition.length % 20) + nullableStr, ); if (unique) { columnDefinition.write(' UNIQUE'); } if (defaultValue != null) { RegExp funcRegex = RegExp( r'^(CURRENT_TIMESTAMP|NOW\(\)|UUID\(\)|RAND\(\))$', caseSensitive: false, ); if (funcRegex.hasMatch(defaultValue.toString())) { columnDefinition.write(" DEFAULT $defaultValue"); if (name == 'updated_at' && defaultValue.toString().toUpperCase() == 'CURRENT_TIMESTAMP') { columnDefinition.write(" ON UPDATE CURRENT_TIMESTAMP"); } } else { if (defaultValue is int || defaultValue is bool) { columnDefinition.write(" DEFAULT $defaultValue"); } else { columnDefinition.write(" DEFAULT '$defaultValue'"); } } } if (comment != null) { columnDefinition.write(" COMMENT '$comment'"); } if (collation != null) { columnDefinition.write(" COLLATE $collation"); } if (expression != null) { columnDefinition.write(' GENERATED ALWAYS AS ($expression)'); } if (virtuality != null) { columnDefinition.write(' $virtuality'); } if (increment) { columnDefinition.write(' AUTO_INCREMENT'); } _queries.add(columnDefinition.toString()); } @override void primary(String columnName, [String algorithm = 'BTREE']) { _primaryField = columnName; _primaryAlgorithm = algorithm; } @override void index(ColumnIndex type, String name, List columns) { if (type == ColumnIndex.indexKey) { _indexes.add('INDEX `$name` (${columns.map((e) => "`$e`").join(',')})'); } else { _indexes.add( '${type.name.toUpperCase()} INDEX `$name` (${columns.map((e) => "`$e`").join(',')})', ); } } @override void foreign( String columnName, String referencesTable, String referencesColumn, { bool constrained = true, String onUpdate = 'CASCADE', String onDelete = 'CASCADE', }) { String constraint = ''; if (constrained) { constraint = 'CONSTRAINT FK_${_tableName}_$referencesTable '; } final fk = '${constraint}FOREIGN KEY (`$columnName`) REFERENCES `$referencesTable` (`$referencesColumn`) ON UPDATE $onUpdate ON DELETE $onDelete'; _foreignKeys.add(fk); } @override List get queries => List.unmodifiable(_queries); @override List get foreignKeys => List.unmodifiable(_foreignKeys); @override List get indexes => List.unmodifiable(_indexes); @override String get primaryField => _primaryField; @override String get primaryAlgorithm => _primaryAlgorithm; String get tableName => _tableName; @override void reset() { _queries.clear(); _foreignKeys.clear(); _indexes.clear(); _columnDefinitions.clear(); _compositeUniqueConstraints.clear(); _compositeIndexes.clear(); _primaryField = ''; _primaryAlgorithm = ''; _tableName = ''; } void addCompositeUniqueConstraint( String constraintName, List columns, ) { if (_compositeUniqueConstraints.containsKey(constraintName)) { _compositeUniqueConstraints[constraintName]!.addAll(columns); } else { _compositeUniqueConstraints[constraintName] = List.from(columns); } } void addCompositeIndex( String indexName, String columnName, ColumnIndex type, ) { if (_compositeIndexes.containsKey(indexName)) { _compositeIndexes[indexName]!['columns'].add(columnName); } else { _compositeIndexes[indexName] = { 'columns': [columnName], 'type': type, }; } } String generateCreateTableSql(String tableName, {bool ifNotExists = false}) { _finalizeColumnDefinitions(); final query = StringBuffer(); String createClause = ifNotExists ? 'CREATE TABLE IF NOT EXISTS' : 'CREATE TABLE'; query.writeln('$createClause `$tableName` ('); query.write(_queries.join(',\n')); if (_primaryField.isNotEmpty) { query.write(',\n PRIMARY KEY (`$_primaryField`)'); } if (_indexes.isNotEmpty) { for (String index in _indexes) { query.write(',\n $index'); } } if (_foreignKeys.isNotEmpty) { for (String fk in _foreignKeys) { query.write(',\n $fk'); } } query.write('\n)'); return query.toString(); } String generateCreateAlterSql( String tableName, { bool ifNotExists = false, String beforeColumn = '', String afterColumn = '', }) { _finalizeColumnDefinitions(); final clauses = []; for (final colDef in _queries) { var clause = 'ADD COLUMN ${colDef.trim()}'; if (beforeColumn.isNotEmpty) clause += ' BEFORE `$beforeColumn`'; if (afterColumn.isNotEmpty) clause += ' AFTER `$afterColumn`'; clauses.add(clause); } if (_primaryField.isNotEmpty) { clauses.add('ADD PRIMARY KEY (`$_primaryField`)'); } for (final idx in _indexes) { clauses.add('ADD $idx'); } for (final fk in _foreignKeys) { clauses.add('ADD $fk'); } final buffer = StringBuffer(); buffer.writeln('ALTER TABLE `$tableName`'); for (var i = 0; i < clauses.length; i++) { final sep = i == clauses.length - 1 ? '' : ','; buffer.writeln(' ${clauses[i]}$sep'); } return buffer.toString(); } String generateDropTableSql(String tableName, {bool ifExists = false}) { String dropClause = ifExists ? 'DROP TABLE IF EXISTS' : 'DROP TABLE'; return '$dropClause `$tableName`'; } } ================================================ FILE: lib/src/database/migration/builders/table_definition.dart ================================================ import 'dart:async'; import '../contracts/database_adapter_interface.dart'; import '../migration_connection.dart'; class TableDefinition implements Future { final String _tableName; final Future Function() _createFunction; final MigrationConnection? _connection; final DatabaseAdapterInterface? _adapter; String? _engine; String? _comment; String? _charset; String? _collation; int? _autoIncrement; TableDefinition( this._tableName, this._createFunction, { MigrationConnection? connection, DatabaseAdapterInterface? adapter, }) : _connection = connection, _adapter = adapter; TableDefinition engine(String engine) { _engine = engine; return this; } TableDefinition comment(String comment) { _comment = comment; return this; } TableDefinition charset(String charset) { _charset = charset; return this; } TableDefinition collate(String collation) { _collation = collation; return this; } TableDefinition autoIncrement(int startValue) { _autoIncrement = startValue; return this; } Future _getExecutionFuture() async { await _createFunction(); await _applyTableOptions(); } @override Future then( FutureOr Function(void value) onValue, { Function? onError, }) { return _getExecutionFuture().then(onValue, onError: onError); } @override Future catchError( Function onError, { bool Function(Object error)? test, }) { return _getExecutionFuture().catchError(onError, test: test); } @override Future whenComplete(FutureOr Function() action) { return _getExecutionFuture().whenComplete(action); } @override Future timeout( Duration timeLimit, { FutureOr Function()? onTimeout, }) { return _getExecutionFuture().timeout(timeLimit, onTimeout: onTimeout); } @override Stream asStream() { return _getExecutionFuture().asStream(); } Future _applyTableOptions() async { if (_engine == null && _comment == null && _charset == null && _collation == null && _autoIncrement == null) { return; // No options to apply } final options = []; if (_engine != null) { options.add('ENGINE=$_engine'); } if (_charset != null) { options.add('DEFAULT CHARSET=$_charset'); } if (_collation != null) { options.add('COLLATE=$_collation'); } if (_comment != null) { options.add("COMMENT='$_comment'"); } if (_autoIncrement != null) { options.add('AUTO_INCREMENT=$_autoIncrement'); } if (options.isNotEmpty && _connection?.connection != null) { String alterSql = 'ALTER TABLE `$_tableName` ${options.join(', ')}'; if (_adapter != null) { alterSql = _adapter.adaptQuery(alterSql); } await _connection!.connection!.execute(alterSql); } } } ================================================ FILE: lib/src/database/migration/contracts/database_adapter_interface.dart ================================================ abstract class DatabaseAdapterInterface { String get driverName; String adaptQuery(String query); String getMigrationsTableSql(); bool supports(String driver); String escapeIdentifier(String identifier); String formatValue(dynamic value); } ================================================ FILE: lib/src/database/migration/contracts/migration_connection_interface.dart ================================================ import '../../../contract/database/_connectors/_database_connection.dart'; abstract class MigrationConnectionInterface { DatabaseConnection? get connection; String? get driver; Future setup(Map databaseConfig); Future closeConnection(); Future truncateMigration(); } ================================================ FILE: lib/src/database/migration/contracts/schema_interface.dart ================================================ import '../../../enum/column_index.dart'; abstract class SchemaInterface { void addColumn( String name, String type, { bool nullable = false, dynamic length, bool unsigned = false, bool zeroFill = false, dynamic defaultValue, String? comment, String? collation, String? expression, String? virtuality, bool increment = false, bool unique = false, }); void primary(String columnName, [String algorithm = 'BTREE']); void index(ColumnIndex type, String name, List columns); void foreign( String columnName, String referencesTable, String referencesColumn, { bool constrained = true, String onUpdate = 'CASCADE', String onDelete = 'CASCADE', }); List get queries; List get foreignKeys; List get indexes; String get primaryField; String get primaryAlgorithm; void reset(); } ================================================ FILE: lib/src/database/migration/migration.dart ================================================ import 'dart:io'; import 'package:meta/meta.dart'; import 'package:vania/src/exception/query_exception.dart'; import 'package:vania/src/utils/functions.dart' show toSnakeCase; import 'builders/schema.dart'; import 'builders/table_definition.dart'; import 'contracts/database_adapter_interface.dart'; import 'migration_connection.dart'; abstract class Migration { late final MigrationConnection _connection; late final Schema _schemaBuilder; late final DatabaseAdapterInterface? _adapter; String get migrationName => toSnakeCase(runtimeType.toString()); Migration() { _connection = MigrationConnection(); _schemaBuilder = Schema(); _adapter = _connection.adapter; } @mustBeOverridden Future up(); @mustBeOverridden Future down(); TableDefinition create( String tableName, Function(Schema) callback, [ bool ifNotExists = false, ]) { _schemaBuilder.reset(); _schemaBuilder.setTableName(tableName); Future createFunction() async { callback(_schemaBuilder); String sql = _schemaBuilder.generateCreateTableSql( tableName, ifNotExists: ifNotExists, ); if (_adapter != null && _adapter.driverName == 'pgsql') { final postgresAdapter = _adapter as dynamic; if (postgresAdapter.executeStatements != null) { await postgresAdapter.executeStatements(sql, ( List statements, ) async { for (String statement in statements) { await _connection.connection!.execute(statement); } }); return; } } if (_adapter != null) { sql = _adapter.adaptQuery(sql); } try { await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('Error executing statement: $sql\nError: ${e.cause}'); exit(0); } } return TableDefinition( tableName, createFunction, connection: _connection, adapter: _adapter, ); } @Deprecated('createTableIfNotExists will be deprecated in version 1.1.0') TableDefinition createTableIfNotExists( String tableName, Function(Schema) callback, ) { _schemaBuilder.reset(); _schemaBuilder.setTableName(tableName); Future createFunction() async { callback(_schemaBuilder); String sql = _schemaBuilder.generateCreateTableSql( tableName, ifNotExists: true, ); if (_adapter != null && _adapter.driverName == 'pgsql') { final postgresAdapter = _adapter as dynamic; if (postgresAdapter.executeStatements != null) { await postgresAdapter.executeStatements(sql, ( String statement, ) async { try { await _connection.connection!.execute(statement); } on QueryException catch (e) { stderr.writeln( 'Error executing statement: $statement\nError: ${e.cause}', ); exit(0); } }); return; } } if (_adapter != null) { sql = _adapter.adaptQuery(sql); } try { await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('Error executing statement: $sql\nError: ${e.cause}'); exit(0); } } return TableDefinition( tableName, createFunction, connection: _connection, adapter: _adapter, ); } TableDefinition alterColumn( String table, Function(Schema) callback, { String beforeColumn = '', String afterColumn = '', }) { _schemaBuilder.reset(); _schemaBuilder.setTableName(table); Future createFunction() async { callback(_schemaBuilder); try { String sql = _schemaBuilder.generateCreateAlterSql( table, afterColumn: afterColumn, beforeColumn: beforeColumn, ); if (_adapter != null) { sql = _adapter.adaptQuery(sql); } await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('${e.cause}'); exit(0); } } return TableDefinition( table, createFunction, connection: _connection, adapter: _adapter, ); } Future drop(String tableName) async { String sql = _schemaBuilder.generateDropTableSql(tableName, ifExists: true); if (_adapter?.driverName == 'mysql') { sql = 'SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;$sql;SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;'; } if (_adapter?.driverName == 'pgsql') { sql = '$sql CASCADE'; } if (_adapter != null) { sql = _adapter.adaptQuery(sql); } try { await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('Error executing statement: $sql\nError: ${e.cause}'); exit(0); } } @Deprecated('dropTableIfExists will be deprecated in version 1.1.0') Future dropTableIfExists(String tableName) async { String sql = _schemaBuilder.generateDropTableSql(tableName, ifExists: true); if (_adapter?.driverName == 'mysql') { sql = 'SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;${sql}SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;'; } if (_adapter != null) { sql = _adapter.adaptQuery(sql); } try { await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('Error executing statement: $sql\nError: ${e.cause}'); exit(0); } } Future execute(String sql) async { if (_adapter != null) { sql = _adapter.adaptQuery(sql); } try { await _connection.connection!.execute(sql); } on QueryException catch (e) { stderr.writeln('Error executing statement: $sql\nError: ${e.cause}'); exit(0); } } } ================================================ FILE: lib/src/database/migration/migration_connection.dart ================================================ import 'dart:io'; import '../../contract/database/_connectors/_database_connection.dart'; import '../../env_handler/env.dart'; import '../../exception/database_exception.dart'; import '../../exception/invalid_argument_exception.dart'; import '../_connection_manager.dart'; import '../_database_utils/_db_config.dart'; import 'adapters/mysql_adapter.dart'; import 'adapters/postgresql_adapter.dart'; import 'adapters/sqlite_adapter.dart'; import 'contracts/database_adapter_interface.dart'; import 'contracts/migration_connection_interface.dart'; class MigrationConnection implements MigrationConnectionInterface { static final MigrationConnection _singleton = MigrationConnection._internal(); DatabaseConnection? _dbConnection; String? _driver; DatabaseAdapterInterface? _adapter; factory MigrationConnection() { Env().load(); return _singleton; } MigrationConnection._internal(); @override DatabaseConnection? get connection => _dbConnection; @override String? get driver => _driver; DatabaseAdapterInterface? get adapter => _adapter; @override Future setup(Map databaseConfig) async { try { final connectionManager = ConnectionManager(); connectionManager.defaultConnection = databaseConfig['default']; Map connections = databaseConfig['connections']; _driver = databaseConfig['default']; await connectionManager.connect( _createDBConfig(connections[_driver]), _driver!, ); _dbConnection = connectionManager.connection(_driver); if (_dbConnection == null) { stderr.writeln('A database must be specified.'); exit(1); } _adapter = _createAdapter(_driver!); String migrationSql = _adapter!.getMigrationsTableSql(); await _dbConnection!.execute(migrationSql); } on InvalidArgumentException catch (e) { stderr.writeln(e.message); exit(1); } on DatabaseException catch (e) { stderr.writeln(e.message); stderr.writeln(e.cause); exit(1); } catch (e) { stderr.write(e.toString()); exit(1); } } @override Future truncateMigration() async { if (_dbConnection == null) { stderr.writeln('Database connection not established'); exit(1); } try { if (_adapter?.supports('pgsql') == true) { await _dbConnection!.execute('TRUNCATE "migrations"'); } else if (_adapter?.supports('sqlite') == true) { await _dbConnection!.execute('DELETE FROM "migrations"'); } else { await _dbConnection!.execute('TRUNCATE `migrations`'); } } catch (e) { stderr.writeln('Failed to truncate migrations table: $e'); exit(1); } } @override Future closeConnection() async { try { await _dbConnection?.close(); _dbConnection = null; _driver = null; _adapter = null; } catch (e) { stderr.writeln('Failed to close connection: $e'); exit(1); } } DBConfig _createDBConfig(Map config) { return DBConfig( driver: config['driver'] ?? '', host: config['host'] ?? '', port: config['port'] ?? 0, database: config['database'] ?? '', username: config['username'] ?? '', password: config['password'] ?? '', sslMode: config['sslmode'] ?? false, collation: config['collation'] ?? '', pool: false, poolSize: 0, filePath: config['file_path'] ?? '', openInMemorySQLite: config['openInMemorySQLite'] ?? false, ); } DatabaseAdapterInterface _createAdapter(String driver) { final normalizedDriver = driver.toLowerCase(); if (normalizedDriver == 'mysql') { return MySqlAdapter(); } else if (normalizedDriver == 'pgsql' || normalizedDriver == 'postgresql' || normalizedDriver == 'postgres') { return PostgreSqlAdapter(); } else if (normalizedDriver == 'sqlite' || normalizedDriver == 'sqlite3') { return SqliteAdapter(); } return MySqlAdapter(); } } ================================================ FILE: lib/src/database/migration/runners/migration_runner.dart ================================================ import 'dart:io'; import 'package:vania/query_builder.dart'; import '../../../utils/functions.dart'; import '../migration.dart'; import '../migration_connection.dart'; class MigrationRunner { int? _currentBatch; final Map _migrations = {}; Future _runUp(String migrationName, Function migrationCallback) async { final isExecuted = await _isMigrationExecuted(migrationName); if (isExecuted) { stderr.writeln('Migration $migrationName already executed, skipping...'); return; } final stopwatch = Stopwatch()..start(); try { await migrationCallback(); final batchNumber = await _getNextBatchNumber(); await _recordMigrationWithBatch(migrationName, batchNumber); stopwatch.stop(); stderr.writeln( ' Migration $migrationName executed ....................................\x1B[32m ${stopwatch.elapsedMilliseconds}ms DONE\x1B[0m', ); } catch (e) { stopwatch.stop(); if (e is QueryException) { stderr.writeln(e.cause); } else { stderr.writeln(e); } stderr.writeln( '❌ Migration $migrationName failed ......................................\x1B[31m ${stopwatch.elapsedMilliseconds}ms FAILED\x1B[0m', ); exit(1); } } MigrationRunner migrationRegister(List migrations) { _migrations.clear(); for (var migration in migrations) { String name = migration.migrationName; if (!_migrations.containsKey(name)) { _migrations[name] = migration; } } return this; } Future run(List args) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } if (args.contains('--fresh')) { await _fresh(_migrations.values.toList()); } else if (args.contains('--install')) { await _install(); } else if (args.contains('--refresh')) { await _refresh(_migrations); } else if (args.contains('--reset')) { await _reset(_migrations); } else if (args.contains('--rollback')) { int? steps = args.contains('--steps') ? int.tryParse(args[args.indexOf('--steps') + 1]) : null; int? batch = args.contains('--batch') ? int.tryParse(args[args.indexOf('--batch') + 1]) : null; await _rollback(_migrations, steps: steps, batch: batch); } else { for (final migration in _migrations.values) { await _runUp(migration.migrationName, migration.up); } stderr.writeln('✅ All migrations executed successfully!'); } } Future _runDown( String migrationName, Function migrationCallback, ) async { final stopwatch = Stopwatch()..start(); try { await Future.delayed(Duration(milliseconds: 30)); await migrationCallback(); stopwatch.stop(); stderr.writeln( ' Migration $migrationName rolled back....................................\x1B[32m ${stopwatch.elapsedMilliseconds}ms DONE\x1B[0m', ); } catch (e) { stopwatch.stop(); stderr.writeln( ' Migration $migrationName failed ......................................\x1B[31m ${stopwatch.elapsedMilliseconds}ms FAILED\x1B[0m', ); exit(1); } } Future _isMigrationExecuted(String migrationName) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { final snakeCaseName = toSnakeCase(migrationName); final result = await MigrationConnection().connection!.select( "SELECT id FROM migrations WHERE migration='$snakeCaseName'", ); return result.isNotEmpty; } catch (e) { if (e is QueryException) { stderr.writeln( '❌ Failed to check if migration is executed: ${e.cause}', ); } else { stderr.writeln('❌ Failed to check if migration is executed: $e'); } exit(1); } } Future _recordMigrationWithBatch( String migrationName, int batch, ) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { final snakeCaseName = toSnakeCase(migrationName); String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'INSERT INTO `migrations` (`migration`, `batch`) VALUES (\'$snakeCaseName\', $batch)'; } else { sql = 'INSERT INTO "migrations" ("migration", "batch") VALUES (\'$snakeCaseName\', $batch)'; } await Future.delayed(Duration(milliseconds: 100)); await MigrationConnection().connection!.execute(sql); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to record migration with batch: ${e.cause}'); } else { stderr.writeln('❌ Failed to record migration with batch: $e'); } exit(1); } } Future _removeMigrationRecord(String migrationName) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { final snakeCaseName = toSnakeCase(migrationName); String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'DELETE FROM `migrations` WHERE `migration`=\'$snakeCaseName\''; } else { sql = 'DELETE FROM "migrations" WHERE "migration"=\'$snakeCaseName\''; } await Future.delayed(Duration(milliseconds: 50)); await MigrationConnection().connection!.execute(sql); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to remove migration record: ${e.cause}'); } else { stderr.writeln('❌ Failed to remove migration record: $e'); } exit(1); } } Future _getNextBatchNumber() async { if (_currentBatch != null) { return _currentBatch!; } final currentBatch = await _getCurrentBatchNumber(); _currentBatch = currentBatch + 1; return _currentBatch!; } Future _getCurrentBatchNumber() async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'SELECT COALESCE(MAX(`batch`), 0) as max_batch FROM `migrations`'; } else { sql = 'SELECT COALESCE(MAX("batch"), 0) as max_batch FROM "migrations"'; } await Future.delayed(Duration(milliseconds: 10)); final result = await MigrationConnection().connection!.select(sql); if (result.isNotEmpty) { return int.parse(result.first['max_batch'].toString()); } return 0; } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to get current batch number: ${e.cause}'); } else { stderr.writeln('❌ Failed to get current batch number: $e'); } exit(1); } } Future> _getMigrationsFromBatch(int batch) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'SELECT `migration` FROM `migrations` WHERE `batch`=$batch ORDER BY "id" DESC'; } else { sql = 'SELECT "migration" FROM "migrations" WHERE "batch"=$batch ORDER BY "id" DESC'; } final result = await MigrationConnection().connection!.select(sql); return result.map((row) => row['migration'] as String).toList(); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to get migrations from batch: ${e.cause}'); } else { stderr.writeln('❌ Failed to get migrations from batch: $e'); } exit(1); } } Future _fresh(List migrations) async { stderr.writeln('🔄 Running fresh migration...'); try { await MigrationConnection().truncateMigration(); _currentBatch = null; for (final migration in _migrations.values) { await _runDown(migration.migrationName, migration.down); } stderr.writeln('📦 Running all migrations...'); for (final migration in migrations) { await _runUp(migration.migrationName, migration.up); } stderr.writeln('✅ Fresh migration completed successfully!'); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to run fresh migration: ${e.cause}'); } else { stderr.writeln('❌ Failed to run fresh migration: $e'); } exit(1); } } Future _install() async { stderr.writeln('📋 Installing migration repository...'); try { if (MigrationConnection().adapter != null) { String migrationSql = MigrationConnection().adapter! .getMigrationsTableSql(); await MigrationConnection().connection!.execute(migrationSql); stderr.writeln('✅ Migration repository installed successfully!'); } else { stderr.writeln('❌ Migration repository installation failed!'); exit(1); } } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to install migration repository: ${e.cause}'); } else { stderr.writeln('❌ Failed to install migration repository: $e'); } exit(1); } } Future _refresh(Map migrations) async { stderr.writeln('🔄 Refreshing migrations...'); try { await _reset(migrations); _currentBatch = null; stderr.writeln('📦 Re-running all migrations...'); for (final migration in migrations.values) { await _runUp(migration.migrationName, migration.up); } stderr.writeln('✅ Migration refresh completed successfully!'); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to refresh migrations: ${e.cause}'); } else { stderr.writeln('❌ Failed to refresh migrations: $e'); } exit(1); } } Future _reset(Map migrations) async { stderr.writeln('⏪ Resetting all migrations...'); try { final allMigrations = await _getAllMigrationsInReverseOrder(); if (allMigrations.isEmpty) { stderr.writeln('ℹ️ No migrations to reset.'); return; } for (final migrationName in allMigrations) { stderr.writeln('⏪ Rolling back: $migrationName'); final migration = migrations[migrationName]; if (migration != null) { await _runDown(migrationName, migration.down); } await _removeMigrationRecord(migrationName); } stderr.writeln('✅ All migrations reset successfully!'); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to reset migrations: ${e.cause}'); } else { stderr.writeln('❌ Failed to reset migrations: $e'); } exit(1); } } Future _rollback( Map migrations, { int? steps, int? batch, }) async { stderr.writeln('⏪ Rolling back migrations...'); try { List migrationsToRollback = []; if (batch != null) { migrationsToRollback = await _getMigrationsFromBatch(batch); stderr.writeln('⏪ Rolling back batch $batch...'); } else if (steps != null) { migrationsToRollback = await _getLastNMigrations(steps); stderr.writeln('⏪ Rolling back last $steps migration(s)...'); } else { final currentBatch = await _getCurrentBatchNumber(); if (currentBatch > 0) { migrationsToRollback = await _getMigrationsFromBatch(currentBatch); stderr.writeln('⏪ Rolling back last batch ($currentBatch)...'); } } if (migrationsToRollback.isEmpty) { stderr.writeln('ℹ️ No migrations to rollback.'); return; } for (final migrationName in migrationsToRollback) { stderr.writeln('⏪ Rolling back: $migrationName'); final migration = migrations[migrationName]; if (migration != null) { await _runDown(migrationName, migration.down); } await _removeMigrationRecord(migrationName); } stderr.writeln('✅ Rollback completed successfully!'); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to rollback migrations: ${e.cause}'); } else { stderr.writeln('❌ Failed to rollback migrations: $e'); } exit(1); } } Future> _getAllMigrationsInReverseOrder() async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'SELECT `migration` FROM `migrations` ORDER BY `id` DESC'; } else { sql = 'SELECT "migration" FROM "migrations" ORDER BY "id" DESC'; } final result = await MigrationConnection().connection!.select(sql); return result.map((row) => row['migration'] as String).toList(); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to get all migrations: ${e.cause}'); } else { stderr.writeln('❌ Failed to get all migrations: $e'); } exit(1); } } Future> _getLastNMigrations(int n) async { if (MigrationConnection().connection == null) { stderr.writeln('Database connection not established'); exit(1); } try { String sql; if (MigrationConnection().adapter?.driverName == 'mysql') { sql = 'SELECT `migration` FROM `migrations` ORDER BY `id` DESC LIMIT $n'; } else { sql = 'SELECT "migration" FROM "migrations" ORDER BY "id" DESC LIMIT $n'; } final result = await MigrationConnection().connection!.select(sql); return result.map((row) => row['migration'] as String).toList(); } catch (e) { if (e is QueryException) { stderr.writeln('❌ Failed to get last $n migrations: ${e.cause}'); } else { stderr.writeln('❌ Failed to get last $n migrations: $e'); } exit(1); } } } ================================================ FILE: lib/src/database/monitoring/database_monitor.dart ================================================ import 'dart:async'; import 'package:meta/meta.dart'; enum AlertType { slowQuery, highConnectionUsage, connectionError, deadlock } class DatabaseAlert { final AlertType type; final String message; final Map details; DatabaseAlert({ required this.type, required this.message, required this.details, }); } class QueryMetrics { final String sql; final Duration executionTime; final DateTime timestamp; QueryMetrics({ required this.sql, required this.executionTime, required this.timestamp, }); } class PerformanceStats { final Duration averageQueryTime; final int totalQueries; final int slowQueries; final Duration peakExecutionTime; PerformanceStats({ required this.averageQueryTime, required this.totalQueries, required this.slowQueries, required this.peakExecutionTime, }); } class DatabaseMonitor { static final DatabaseMonitor _instance = DatabaseMonitor._internal(); factory DatabaseMonitor() => _instance; DatabaseMonitor._internal(); final Map> _queryMetrics = {}; final Map _connectionMetrics = {}; final StreamController _alertController = StreamController.broadcast(); Stream get alerts => _alertController.stream; // Performance thresholds static const Duration slowQueryThreshold = Duration(milliseconds: 100); static const int highConnectionUsageThreshold = 80; void recordQuery(String connectionId, String sql, Duration executionTime) { final metrics = QueryMetrics( sql: sql, executionTime: executionTime, timestamp: DateTime.now(), ); _queryMetrics.putIfAbsent(connectionId, () => []).add(metrics); // Check for slow queries if (executionTime > slowQueryThreshold) { _alertController.add( DatabaseAlert( type: AlertType.slowQuery, message: 'Slow query detected', details: { 'sql': sql, 'execution_time': executionTime.inMilliseconds, 'threshold': slowQueryThreshold.inMilliseconds, }, ), ); } if (_queryMetrics[connectionId]!.length > 1000) { _queryMetrics[connectionId]!.removeAt(0); } } void updateConnectionMetrics(String connectionId, ConnectionMetrics metrics) { _connectionMetrics[connectionId] = metrics; if (metrics.usagePercentage > highConnectionUsageThreshold) { _alertController.add( DatabaseAlert( type: AlertType.highConnectionUsage, message: 'High connection pool usage detected', details: { 'usage_percentage': metrics.usagePercentage, 'active_connections': metrics.activeConnections, 'max_connections': metrics.maxConnections, }, ), ); } } List getSlowQueries(String connectionId) { return _queryMetrics[connectionId] ?.where((m) => m.executionTime > slowQueryThreshold) .toList() ?? []; } Map getPerformanceStats() { final stats = {}; for (final entry in _queryMetrics.entries) { final queries = entry.value; if (queries.isEmpty) continue; final totalTime = queries.fold( Duration.zero, (sum, metric) => sum + metric.executionTime, ); final peakTime = queries .map((m) => m.executionTime) .reduce((max, time) => time > max ? time : max); final slowCount = queries .where((m) => m.executionTime > slowQueryThreshold) .length; stats[entry.key] = PerformanceStats( averageQueryTime: totalTime ~/ queries.length, totalQueries: queries.length, slowQueries: slowCount, peakExecutionTime: peakTime, ); } return stats; } void clearMetrics(String connectionId) { _queryMetrics.remove(connectionId); _connectionMetrics.remove(connectionId); } @visibleForTesting void reset() { _queryMetrics.clear(); _connectionMetrics.clear(); } } class ConnectionMetrics { final int activeConnections; final int maxConnections; final int usagePercentage; ConnectionMetrics({ required this.activeConnections, required this.maxConnections, required this.usagePercentage, }); } ================================================ FILE: lib/src/database/orm/belongs_to.dart ================================================ import 'package:vania/src/contract/orm/relation.dart'; class BelongsTo extends Relation { BelongsTo({ required super.related, required super.parent, super.foreignKey, super.localKey, }); @override List> match( List> models, List> results, String relation, ) => matchOneOrMany( models, results, relation, foreignKey ?? '${related.runtimeType.toString()}_id', localKey, ); } ================================================ FILE: lib/src/database/orm/belongs_to_many.dart ================================================ import 'package:vania/src/contract/orm/relation.dart'; class BelongsToMany extends Relation { final String pivotTable; final String parentPivotKey; final String relatedPivotKey; final String parentLocalKey; final String relatedLocalKey; final List pivotFields; BelongsToMany({ required super.parent, required super.related, required this.pivotTable, required this.parentPivotKey, required this.relatedPivotKey, this.parentLocalKey = 'id', this.relatedLocalKey = 'id', this.pivotFields = const [], }); @override List> match( List> parents, List> rows, String relationName, ) { return matchToMany( parents, rows, relationName, parentLocalKey, parentPivotKey, relatedPivotKey, pivotFields: pivotFields, ); } } ================================================ FILE: lib/src/database/orm/has_many.dart ================================================ import 'package:vania/src/contract/orm/relation.dart'; class HasMany extends Relation { HasMany({ required super.related, required super.parent, super.foreignKey, super.localKey, }); @override List> match( List> models, List> results, String relation, ) => matchMany( models, results, relation, localKey, foreignKey ?? '${related.runtimeType.toString()}_id', ); } ================================================ FILE: lib/src/database/orm/has_one.dart ================================================ import 'package:vania/src/contract/orm/relation.dart'; class HasOne extends Relation { HasOne({ required super.related, required super.parent, super.foreignKey, required super.localKey, }); @override List> match( List> models, List> results, String relation, ) => matchOneOrMany( models, results, relation, localKey, foreignKey ?? '${related.runtimeType.toString().toLowerCase()}_id', ); } ================================================ FILE: lib/src/database/orm/model.dart ================================================ import 'package:meta/meta.dart'; import 'package:vania/query_builder.dart'; import 'package:vania/src/contract/database/query_builder/query_builder.dart'; import 'package:vania/src/contract/orm/morph_relation.dart'; import 'package:vania/src/contract/orm/relation.dart'; import 'package:vania/src/database/_database_utils/_singularize.dart'; import 'package:vania/src/database/orm/polymorphic/morphed_by_many.dart'; import 'package:vania/src/database/query_builder/_query_builder_impl.dart'; 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'; import 'has_one.dart'; import 'polymorphic/morph_many.dart'; import 'polymorphic/morph_one.dart'; import 'polymorphic/morph_to.dart'; import 'polymorphic/morph_to_many.dart'; abstract class Model extends QueryBuilderImpl { @protected Map attributes = {}; final Map _relations = {}; List<_RelationQuery> _withRelation = []; String get defaultConnection => _connection; String _connection = 'mysql'; @protected String get createdAt => 'created_at'; @protected String get deletedAt => 'deleted_at'; @protected String get updatedAt => 'updated_at'; @protected List get fillable => []; @protected List get guarded => []; @protected List get hidden => []; @protected bool get incrementing => true; @protected String get keyType => 'int'; @protected String get primaryKey => 'id'; @protected String? _table; Model get query => connection(defaultConnection).table('$tablePrefix$tableName') as Model; @protected bool get softDeletes => false; @protected String get tablePrefix => ''; @protected @override String get getTable => _table ?? tableName; @protected String get tableName => toSnakeCase(Pluralize().make(runtimeType.toString())).toLowerCase(); set tableName(String table) { _table = table; } @protected bool get timestamps => true; bool _relationsRegistered = false; /// Override this method to define model relationships /// This method is called automatically when include() is used void registerRelations() {} @override Future avg(String column) async { if (softDeletes) { whereNull(deletedAt); } return super.avg(column); } void belongsTo( String name, Model model, { String? foreignKey, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, BelongsTo( related: model, parent: this, foreignKey: foreignKey ?? '${model.runtimeType.toString()}_id'.toLowerCase(), localKey: localKey, ), ), ]); } void belongsToMany( String name, Model model, { String? pivotTable, required parentPivotKey, required relatedPivotKey, parentLocalKey = 'id', relatedLocalKey = 'id', }) { _relations.addEntries([ MapEntry( name, BelongsToMany( related: model, parent: this, pivotTable: pivotTable ?? '${Singularize.make(model.runtimeType.toString())}_${Singularize.make(runtimeType.toString())}' .toLowerCase(), parentPivotKey: parentPivotKey, relatedPivotKey: relatedPivotKey, parentLocalKey: parentLocalKey, relatedLocalKey: relatedLocalKey, ), ), ]); } @override Future chunk( int chunk, void Function(List> data) callback, ) async { if (softDeletes) { whereNull(deletedAt); } super.chunk(chunk, (List> data) async { for (Map map in data) { map.removeWhere((key, _) => hidden.contains(key)); } final result = await _loadRelations(data); callback(result); }); } @override Future chunkById( int chunk, void Function(List> data) callback, [ String column = 'id', ]) async { if (softDeletes) { whereNull(deletedAt); } super.chunkById(chunk, (List> data) async { for (Map map in data) { map.removeWhere((key, _) => hidden.contains(key)); } final result = await _loadRelations(data); callback(result); }, column); } @override Model connection([String? connection]) { _setdefaultConnection(connection ?? 'mysql'); return this; } @override Future count([String columns = '*']) async { if (softDeletes) { whereNull(deletedAt); } return super.count(columns); } Future> create(Map values) async { if (timestamps) { values[createdAt] = DateTime.now(); values[updatedAt] = DateTime.now(); } final id = await insertGetId(values); final result = await find(id); return result!; } @override Future delete() async { try { if (softDeletes) { final now = DateTime.now(); super.update({deletedAt: now, updatedAt: now}); } else { super.delete(); } return true; } catch (_) { return false; } } @override Future doesntExist() async { if (softDeletes) { whereNull(deletedAt); } return super.doesntExist(); } @override Future exists() async { if (softDeletes) { whereNull(deletedAt); } return super.exists(); } Model fill() { for (var key in attributes.keys) { if (fillable.contains(key) && !guarded.contains(key)) { setAttribute(key, attributes[key]); } } return this; } @override Future?> find( dynamic id, { String? byColumnName, List columns = const ['*'], }) async { if (softDeletes) { whereNull(deletedAt); } Map? result = await super.find( id, byColumnName: byColumnName ?? primaryKey, columns: columns, ); attributes = Map.from(result ?? {}); if (result != null) { List> data = [result]; result = (await _loadRelations(data)).first; } return result; } @override Future?> findOrFail( id, { String? byColumnName, List columns = const ['*'], }) async { var result = await find( id, byColumnName: byColumnName ?? primaryKey, columns: columns, ); if (result == null) { throw InvalidArgumentException("Record with id $id not found."); } return result; } @override Future?> first([ List columns = const ['*'], ]) async { if (softDeletes) { whereNull(deletedAt); } super.limit(1).toSql(); final result = await super.first(columns); attributes = Map.from(result ?? {}); if (result == null) { return null; } return (await _loadRelations([result])).first; } @override Future?> firstWhere( String column, [ String? operator = '=', value, List columns = const ['*'], ]) async { if (value == null) { throw InvalidArgumentException( "Invalid input: Value cannot be null. A valid value must be provided for the firstWhere method.", ); } where(column, operator ?? '=', value); return await first(columns); } @override Future>> get([ List columns = const ['*'], ]) async { try { if (softDeletes) { whereNull(deletedAt); } List> result = await super.get(columns); for (Map map in result) { map.removeWhere((key, _) => hidden.contains(key)); } return _loadRelations(result); } catch (e) { throw Exception(e); } } dynamic getAttribute(String key) { return attributes[key]; } dynamic getKey() { return attributes[primaryKey]; } bool hasAttribute(String key) { return attributes.containsKey(key); } void hasMany( String name, Model model, { String? foreignKey, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, HasMany( related: model, parent: this, foreignKey: foreignKey ?? '${runtimeType.toString()}_id'.toLowerCase(), localKey: localKey, ), ), ]); } void hasOne( String name, Model model, { String? foreignKey, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, HasOne( related: model, parent: this, foreignKey: foreignKey ?? '${runtimeType.toString()}_id'.toLowerCase(), localKey: localKey, ), ), ]); } @override Future insert(Map values) async { _validateFieldsForAssignment(values); await super.insert(values); return Future.value(true); } @override Future insertGetId(Map values, [String? sequence]) async { _validateFieldsForAssignment(values); final id = await super.insertGetId(values, sequence); attributes[primaryKey] = id; return id; } bool is_(Model? model) { if (model == null) return false; return model.getKey() == getKey() && model.getTable == getTable; } @override Future max(String column) async { if (softDeletes) { whereNull(deletedAt); } return super.max(column); } @override Future min(String column) async { if (softDeletes) { whereNull(deletedAt); } return super.min(column); } void morphedByMany( String name, Model model, { required String morphKey, required String morphType, required String type, required String pivotTable, required String relatedMorphKey, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, MorphedByMany( parent: this, related: model, morphKey: morphKey, morphType: morphType, pivotTable: pivotTable, relatedMorphKey: relatedMorphKey, type: type, localKey: localKey, ), ), ]); } void morphMany( String name, Model model, { required String morphKey, required String morphType, required String type, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, MorphMany( parent: this, related: model, morphKey: morphKey, morphType: morphType, type: type, localKey: localKey, ), ), ]); } void morphOne( String name, Model model, { required String morphKey, required String morphType, required String type, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, MorphOne( parent: this, related: model, morphKey: morphKey, morphType: morphType, type: type, localKey: localKey, ), ), ]); } void morphTo( String name, Model model, { required String morphKey, required String morphType, required String type, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, MorphTo( parent: this, related: model, morphKey: morphKey, morphType: morphType, type: type, localKey: localKey, ), ), ]); } void morphToMany( String name, Model model, { required String morphKey, required String morphType, required String type, required String pivotTable, required String relatedMorphKey, String localKey = 'id', }) { _relations.addEntries([ MapEntry( name, MorphToMany( parent: this, related: model, morphKey: morphKey, morphType: morphType, pivotTable: pivotTable, relatedMorphKey: relatedMorphKey, type: type, localKey: localKey, ), ), ]); } Model newInstance() { return (runtimeType as dynamic)(); } @override Future> paginate({ int perPage = 15, List columns = const ['*'], String? pageName, int? page, }) async { int currentPage = page ?? getParam('page', 1)!; int total = await count(); final lastPage = (total / perPage).ceil(); final offset = (currentPage - 1) * perPage; super.take(perPage).skip(offset); final pageData = await get(); final isFirst = currentPage == 1; final isLast = currentPage == lastPage; final hasMore = currentPage < lastPage; return PaginatedResult( data: pageData, currentPage: currentPage, perPage: perPage, total: total, lastPage: lastPage, isFirst: isFirst, isLast: isLast, hasMore: hasMore, ).toMap(); } @override Future pluck(String column, [String? key]) async { if (softDeletes) { whereNull(deletedAt); } return super.pluck(column); } void setAttribute(String key, dynamic value) { attributes[key] = value; } @override Future> simplePaginate([ int perPage = 15, List columns = const ['*'], String? pageName, int? page, ]) async { int currentPage = page ?? getParam('page', 1)!; int total = await count(); final lastPage = (total / perPage).ceil(); final offset = (currentPage - 1) * perPage; super.take(perPage).skip(offset); final pageData = await get(); return { 'data': pageData, 'current_page': currentPage, 'per_page': perPage, 'total': total, 'last_page': lastPage, }; } @override Future sum(String column) async { if (softDeletes) { whereNull(deletedAt); } return super.sum(column); } Map toJson() { final Map result = Map.from(attributes); for (var key in hidden) { result.remove(key); } _relations.forEach((key, relation) { if (relation is! Future) { result[key] = relation; } }); return result; } @override Future update(Map values) async { if (values.isEmpty) { throw InvalidArgumentException('Update values cannot be empty'); } _validateFieldsForAssignment(values); if (timestamps) { values[updatedAt] = DateTime.now(); } return super.update(values); } @override Future updateMany( List> updates, String column, ) async { if (updates.isEmpty) return false; if (timestamps) { for (var row in updates) { _validateFieldsForAssignment(row); row.addEntries([MapEntry(updatedAt, DateTime.now())]); } } return super.updateMany(updates, column); } @override Future updateOrInsert( Map search, Map update, ) async { Map data = {} ..addAll(search) ..addAll(update); return upsert(data, search.keys.toList(), update); } @override Future upsert( Map values, List uniqueBy, [ Map? update, ]) async { _validateFieldsForAssignment(values); if (update != null) { _validateFieldsForAssignment(update); } return super.upsert(values, uniqueBy); } @override Future value(String column) async { if (softDeletes) { whereNull(deletedAt); } return super.value(column); } Model include(String relation, [Function(Model qb)? callback]) { if (!_relationsRegistered) { registerRelations(); _relationsRegistered = true; } final rela = _relations[relation]; if (rela != null && rela is MorphTo) { where(rela.morphType, '=', rela.type!); } _withRelation.add(_RelationQuery(relation, callback)); return this; } Future _eagerLoadRelation( List> models, _RelationQuery rq, Function(dynamic data) callBack, ) async { String relation = rq.relation; List wr = relation.split('.'); String primaryRelation = wr.first; List getColumns = ['*']; final relationParts = primaryRelation.split(':'); if (relationParts.length > 1) { primaryRelation = relationParts.first.trim(); final columnsString = relationParts.last.trim(); if (columnsString.isNotEmpty) { getColumns = columnsString .split(',') .map((col) => col.trim()) .where((col) => col.isNotEmpty) .toList(); } } if (!_relations.containsKey(primaryRelation)) { throw InvalidArgumentException( 'Relation $relation not found in $runtimeType', ); } Relation rela = _relations[primaryRelation] as Relation; Model qb = rela.related; if (rq.callback != null) { qb = rq.callback!(qb) as Model; } if (rela is MorphRelation) { if (rela is MorphTo) { Set ids = models.map((m) => m[rela.morphKey]).toSet(); // Early return if no IDs to query if (ids.isEmpty) { callBack(rela.match(models, [], primaryRelation)); return; } qb = qb.whereIn(rela.localKey, ids.toList()) as Model; } else { Set ids = models.map((m) => m[rela.localKey]).toSet(); // Early return if no IDs to query if (ids.isEmpty) { callBack(rela.match(models, [], primaryRelation)); return; } if (rela is MorphToMany || rela is MorphedByMany) { qb = qb .whereIn(rela.morphKey, ids.toList()) .whereEqualTo(rela.morphType, rela.type) .join( rela.related.tableName, '${rela.pivotTable}.${rela.relatedMorphKey}', '=', '${rela.related.tableName}.${rela.localKey}', ) as Model; qb.tableName = rela.pivotTable!; } else { qb = qb .whereIn(rela.morphKey, ids.toList()) .whereEqualTo(rela.morphType, rela.type) as Model; } } } else { late final String getLocalKey; if (rela is BelongsTo) { getLocalKey = rela.foreignKey ?? '${rela.related.runtimeType.toString()}_id'.toLowerCase(); } else { getLocalKey = rela.localKey; } Set ids = models.map((m) => m[getLocalKey]).toSet(); // Early return if no IDs to query if (ids.isEmpty) { callBack(rela.match(models, [], primaryRelation)); return; } if (rela is BelongsToMany) { qb = qb .whereIn( '${rela.pivotTable}.${rela.parentPivotKey}', ids.toList(), ) .join( rela.pivotTable, '${rela.pivotTable}.${rela.relatedPivotKey}', '=', '${rela.related.tableName}.${rela.relatedLocalKey}', ) as Model; } else if (rela is BelongsTo) { qb = qb.whereIn(rela.localKey, ids.toList()) as Model; } else { qb = qb.whereIn(rela.foreignKey!, ids.toList()) as Model; } } late final List> results; if (wr.length > 1) { wr.removeAt(0); results = await qb.include(wr.join('.')).get(getColumns); } else { results = await qb.get(getColumns); } callBack(rela.match(models, results, primaryRelation)); } Future>> _loadRelations( List> result, ) async { if (_withRelation.isNotEmpty) { // Create an immutable copy of relations to load and clear the original list // This prevents concurrent modification when iterating final relationsToLoad = List<_RelationQuery>.unmodifiable(_withRelation); _withRelation = []; for (_RelationQuery relation in relationsToLoad) { await _eagerLoadRelation(result, relation, (callBackResult) { result = callBackResult; }); } } return result; } void _setdefaultConnection(String value) => _connection = value; /// Validates field names for mass assignment according to fillable and guarded rules /// Throws [InvalidArgumentException] if validation fails void _validateFieldsForAssignment(Map values) { List keys = values.keys.toList(); if (fillable.isNotEmpty) { fillable.add(createdAt); fillable.add(updatedAt); fillable.add(deletedAt); } for (String key in keys) { if (guarded.contains(key)) { throw InvalidArgumentException( 'Column $key is not allowed to be filled by guarded', ); } if (guarded.isEmpty && fillable.isEmpty) { throw InvalidArgumentException( 'Column $key is not allowed to be filled', ); } if (!guarded.contains(key) && fillable.isNotEmpty && !fillable.contains(key)) { throw InvalidArgumentException( 'Column $key is not allowed to be filled', ); } } } } class _RelationQuery { final String relation; final Function(Model qb)? callback; _RelationQuery(this.relation, [this.callback]); } ================================================ FILE: lib/src/database/orm/polymorphic/morph_many.dart ================================================ import 'package:vania/src/contract/orm/morph_relation.dart'; class MorphMany extends MorphRelation { MorphMany({ required super.parent, required super.related, required super.morphKey, required super.morphType, super.type, super.localKey = 'id', }); @override List> match( List> models, List> results, String relation, ) => matchMorphMany( models, results, relation, localKey, morphKey, morphType, type ?? related.runtimeType.toString().toLowerCase(), ); } ================================================ FILE: lib/src/database/orm/polymorphic/morph_one.dart ================================================ import 'package:vania/src/contract/orm/morph_relation.dart'; class MorphOne extends MorphRelation { MorphOne({ required super.parent, required super.related, required super.morphKey, required super.morphType, super.type, super.localKey = 'id', }); @override List> match( List> models, List> results, String relation, ) => matchMorphOneOrMany( models, results, relation, localKey, morphKey, morphType, type ?? related.runtimeType.toString().toLowerCase(), ); } ================================================ FILE: lib/src/database/orm/polymorphic/morph_to.dart ================================================ import 'package:vania/src/contract/orm/morph_relation.dart'; class MorphTo extends MorphRelation { MorphTo({ required super.parent, required super.related, required super.morphKey, required super.morphType, super.type, super.localKey = 'id', }); @override List> match( List> models, List> results, String relation, ) { return matchMorphToOne(models, results, relation, morphKey, localKey); } } ================================================ FILE: lib/src/database/orm/polymorphic/morph_to_many.dart ================================================ import 'package:vania/src/contract/orm/morph_relation.dart'; class MorphToMany extends MorphRelation { MorphToMany({ required super.parent, required super.related, required super.morphKey, required super.morphType, required super.pivotTable, required super.relatedMorphKey, super.type, super.localKey = 'id', }); @override List> match( List> models, List> results, String relation, ) { return matchMorphMany( models, results, relation, localKey, morphKey, morphType, type ?? related.runtimeType.toString().toLowerCase(), ); } } ================================================ FILE: lib/src/database/orm/polymorphic/morphed_by_many.dart ================================================ import 'package:vania/src/contract/orm/morph_relation.dart'; class MorphedByMany extends MorphRelation { MorphedByMany({ required super.parent, required super.related, required super.morphKey, required super.morphType, required super.pivotTable, required super.relatedMorphKey, super.type, super.localKey = 'id', }); @override List> match( List> models, List> results, String relation, ) { return matchMorphMany( models, results, relation, localKey, morphKey, morphType, type ?? related.runtimeType.toString().toLowerCase(), ); } } ================================================ FILE: lib/src/database/query_builder/_bulk_operations_builder_impl.dart ================================================ import 'dart:async'; import 'package:meta/meta.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder, ConflictAction; import '../../exception/invalid_argument_exception.dart'; import '_query_builder_impl.dart'; abstract mixin class BulkOperationsBuilderImpl implements QueryBuilder { @protected int _paramCounter = 0; String _nextParamName() { _paramCounter++; return 'p$_paramCounter'; } @override Future merge( List> sourceData, { required List matchOn, ConflictAction whenMatched = ConflictAction.update, ConflictAction whenNotMatched = ConflictAction.ignore, ConflictAction? whenNotMatchedBySource, List? updateColumns, List? insertColumns, Map? additionalValues, }) async { if (sourceData.isEmpty) { throw InvalidArgumentException( 'Source data cannot be empty for merge operation', ); } if (matchOn.isEmpty) { throw InvalidArgumentException( 'Match columns cannot be empty for merge operation', ); } try { final conn = await getConnection(); final paramBindings = {}; final sourceColumns = sourceData.first.keys.toList(); final valueGroups = []; for (var row in sourceData) { final placeholders = sourceColumns .map((column) { final paramName = _nextParamName(); paramBindings[paramName] = row[column]; return ":$paramName"; }) .join(", "); valueGroups.add("($placeholders)"); } final sourceClause = "(VALUES ${valueGroups.join(', ')}) AS source(${sourceColumns.join(', ')})"; final matchConditions = matchOn .map((col) => "$getTable.$col = source.$col") .join(' AND '); String mergeSQL = "MERGE INTO $getTable USING $sourceClause ON $matchConditions"; if (whenMatched == ConflictAction.update) { final columnsToUpdate = updateColumns ?? sourceColumns.where((col) => !matchOn.contains(col)).toList(); final updateSets = columnsToUpdate .map((col) => "$col = source.$col") .join(', '); mergeSQL += " WHEN MATCHED THEN UPDATE SET $updateSets"; } else if (whenMatched == ConflictAction.delete) { mergeSQL += " WHEN MATCHED THEN DELETE"; } if (whenNotMatched != ConflictAction.ignore) { final columnsToInsert = insertColumns ?? sourceColumns; final insertValues = columnsToInsert .map((col) => "source.$col") .join(', '); mergeSQL += " WHEN NOT MATCHED THEN INSERT (${columnsToInsert.join(', ')}) VALUES ($insertValues)"; } if (whenNotMatchedBySource == ConflictAction.delete) { mergeSQL += " WHEN NOT MATCHED BY SOURCE THEN DELETE"; } await conn.execute(mergeSQL, paramBindings); return true; } catch (e) { rethrow; } } @override Future bulkInsert( List> data, { ConflictAction conflictAction = ConflictAction.ignore, List? conflictColumns, List? updateColumns, int batchSize = 1000, bool returnIds = false, }) async { if (data.isEmpty) { throw InvalidArgumentException( 'Data cannot be empty for bulk insert operation', ); } try { final conn = await getConnection(); for (int i = 0; i < data.length; i += batchSize) { final batch = data.skip(i).take(batchSize).toList(); final paramBindings = {}; final columns = batch.first.keys.toList(); final valueGroups = []; for (var row in batch) { final placeholders = columns .map((column) { final paramName = _nextParamName(); paramBindings[paramName] = row[column]; return ":$paramName"; }) .join(", "); valueGroups.add("($placeholders)"); } String sql = "INSERT INTO $getTable (${columns.join(', ')}) VALUES ${valueGroups.join(', ')}"; if (conflictAction == ConflictAction.ignore) { sql = "INSERT IGNORE INTO $getTable (${columns.join(', ')}) VALUES ${valueGroups.join(', ')}"; } else if (conflictAction == ConflictAction.update && conflictColumns != null) { final updateCols = updateColumns ?? columns.where((col) => !conflictColumns.contains(col)).toList(); final updateSets = updateCols .map((col) => "$col = VALUES($col)") .join(', '); sql += " ON DUPLICATE KEY UPDATE $updateSets"; } else if (conflictAction == ConflictAction.replace) { sql = "REPLACE INTO $getTable (${columns.join(', ')}) VALUES ${valueGroups.join(', ')}"; } await conn.execute(sql, paramBindings); } return true; } catch (e) { rethrow; } } @override Future bulkUpdate( List> updates, { required String matchColumn, List? updateColumns, int batchSize = 500, Map? additionalValues, }) async { if (updates.isEmpty) { throw InvalidArgumentException( 'Updates cannot be empty for bulk update operation', ); } try { final conn = await getConnection(); for (int i = 0; i < updates.length; i += batchSize) { final batch = updates.skip(i).take(batchSize).toList(); final columns = updateColumns ?? batch.first.keys.where((key) => key != matchColumn).toList(); final paramBindings = {}; final matchValues = []; final caseClauses = >{}; for (var column in columns) { caseClauses[column] = []; } for (var row in batch) { final matchParamName = _nextParamName(); paramBindings[matchParamName] = row[matchColumn]; matchValues.add(":$matchParamName"); for (var column in columns) { final valueParamName = _nextParamName(); paramBindings[valueParamName] = row[column]; caseClauses[column]!.add( "WHEN :$matchParamName THEN :$valueParamName", ); } } final setClauses = caseClauses.entries.map((entry) { final caseStatement = "CASE $matchColumn ${entry.value.join(' ')} END"; return "${entry.key} = $caseStatement"; }).toList(); if (additionalValues != null) { for (var entry in additionalValues.entries) { final paramName = _nextParamName(); paramBindings[paramName] = entry.value; setClauses.add("${entry.key} = :$paramName"); } } final sql = "UPDATE $getTable SET ${setClauses.join(', ')} WHERE $matchColumn IN (${matchValues.join(', ')})"; await conn.execute(sql, paramBindings); } return true; } catch (e) { rethrow; } } @override Future bulkDelete({ String? column, List? values, int batchSize = 1000, }) async { if (column == null || values == null || values.isEmpty) { throw InvalidArgumentException( 'Column and values must be provided for bulk delete operation', ); } try { final conn = await getConnection(); for (int i = 0; i < values.length; i += batchSize) { final batch = values.skip(i).take(batchSize).toList(); final paramBindings = {}; final placeholders = batch .map((value) { final paramName = _nextParamName(); paramBindings[paramName] = value; return ":$paramName"; }) .join(', '); final sql = "DELETE FROM $getTable WHERE $column IN ($placeholders)"; await conn.execute(sql, paramBindings); } return true; } catch (e) { rethrow; } } @override Future bulkDeleteWhere( List> conditions, { int batchSize = 500, }) async { if (conditions.isEmpty) { throw InvalidArgumentException( 'Conditions cannot be empty for bulk delete operation', ); } try { final conn = await getConnection(); for (int i = 0; i < conditions.length; i += batchSize) { final batch = conditions.skip(i).take(batchSize).toList(); final paramBindings = {}; final orConditions = []; for (var conditionMap in batch) { final andConditions = []; for (var entry in conditionMap.entries) { final paramName = _nextParamName(); paramBindings[paramName] = entry.value; andConditions.add("${entry.key} = :$paramName"); } orConditions.add("(${andConditions.join(' AND ')})"); } final sql = "DELETE FROM $getTable WHERE ${orConditions.join(' OR ')}"; await conn.execute(sql, paramBindings); } return true; } catch (e) { rethrow; } } @override Future batchProcess({ required int batchSize, required Future Function( List> batch, int batchNumber, ) processor, List columns = const ['*'], }) async { int batchNumber = 1; int offset = 0; while (true) { final batchQuery = QueryBuilderImpl() ..table(getTable) ..select(columns) ..limit(batchSize) ..offset(offset); final batch = await batchQuery.get(); if (batch.isEmpty) break; await processor(batch, batchNumber); if (batch.length < batchSize) break; offset += batchSize; batchNumber++; } } @override Future chunkedProcess({ required int chunkSize, required Future>> Function( List> chunk, ) processor, String? destination, List columns = const ['*'], }) async { int offset = 0; while (true) { final chunkQuery = QueryBuilderImpl() ..table(getTable) ..select(columns) ..limit(chunkSize) ..offset(offset); final chunk = await chunkQuery.get(); if (chunk.isEmpty) break; final processedChunk = await processor(chunk); if (destination != null && processedChunk.isNotEmpty) { final insertQuery = QueryBuilderImpl()..table(destination); await insertQuery.insertMany(processedChunk); } if (chunk.length < chunkSize) break; offset += chunkSize; } } @override Future parallelBulkInsert( List> data, { int parallelism = 2, int batchSize = 1000, ConflictAction conflictAction = ConflictAction.ignore, List? conflictColumns, }) async { if (data.isEmpty) { throw InvalidArgumentException( 'Data cannot be empty for parallel bulk insert operation', ); } try { final chunkSize = (data.length / parallelism).ceil(); final futures = >[]; for (int i = 0; i < parallelism; i++) { final start = i * chunkSize; final end = (start + chunkSize).clamp(0, data.length); if (start >= data.length) break; final chunk = data.sublist(start, end); final future = bulkInsert( chunk, conflictAction: conflictAction, conflictColumns: conflictColumns, batchSize: batchSize, ); futures.add(future); } final results = await Future.wait(futures); return results.every((result) => result); } catch (e) { rethrow; } } @override Future transactionalBulkOperation( Future Function() action, ) async { try { return await transaction(() async => await action()); } catch (e) { rethrow; } } @protected void clearBulkOperations() { _paramCounter = 0; } } ================================================ FILE: lib/src/database/query_builder/_cte/_cte_cache.dart ================================================ class CteCache { String? _cachedWithClause; Map? _cachedBindings; int _cteHashCode = 0; bool _isDirty = true; void markDirty() { _isDirty = true; _cachedWithClause = null; _cachedBindings = null; } bool needsUpdate(int currentHashCode) { return _isDirty || _cteHashCode != currentHashCode; } void updateCache( String withClause, Map bindings, int hashCode, ) { _cachedWithClause = withClause; _cachedBindings = Map.from(bindings); _cteHashCode = hashCode; _isDirty = false; } String? get cachedWithClause => _cachedWithClause; Map? get cachedBindings => _cachedBindings != null ? Map.from(_cachedBindings!) : null; void clear() { _cachedWithClause = null; _cachedBindings = null; _cteHashCode = 0; _isDirty = true; } } ================================================ FILE: lib/src/database/query_builder/_cte/_cte_configuration.dart ================================================ import '_cte_feature.dart'; import '_database_type.dart'; class CteConfiguration { final String quoteChar; final DatabaseType databaseType; final bool enableCaching; final int maxCteDepth; final bool allowMaterialized; final bool allowNotMaterialized; final bool allowRecursive; const CteConfiguration({ this.quoteChar = '"', this.databaseType = DatabaseType.postgresql, this.enableCaching = true, this.maxCteDepth = 100, this.allowMaterialized = true, this.allowNotMaterialized = true, this.allowRecursive = true, }); factory CteConfiguration.forDatabase(DatabaseType type) { switch (type) { case DatabaseType.postgresql: return const CteConfiguration( quoteChar: '"', databaseType: DatabaseType.postgresql, allowMaterialized: true, allowNotMaterialized: true, allowRecursive: true, ); case DatabaseType.mysql: return const CteConfiguration( quoteChar: '`', databaseType: DatabaseType.mysql, allowMaterialized: false, allowNotMaterialized: false, allowRecursive: true, ); case DatabaseType.sqlite: return const CteConfiguration( quoteChar: '"', databaseType: DatabaseType.sqlite, allowMaterialized: false, allowNotMaterialized: false, allowRecursive: true, ); } } bool supportsFeature(CteFeature feature) { switch (feature) { case CteFeature.materialized: return allowMaterialized; case CteFeature.notMaterialized: return allowNotMaterialized; case CteFeature.recursive: return allowRecursive; } } } ================================================ FILE: lib/src/database/query_builder/_cte/_cte_definition.dart ================================================ import 'package:vania/src/contract/database/query_builder/query_builder.dart'; import '../_cte_builder_impl.dart'; import '_cte_configuration.dart'; import '_cte_feature.dart'; import '_invalid_cte_configuration_exception.dart'; import '_sql_identifier_escaper.dart'; import '_standard_escaping_strategy.dart'; import '_unsupported_cte_feature_exception.dart'; class CteDefinition { final String name; final QueryBuilder query; final bool isRecursive; final bool isMaterialized; final bool isNotMaterialized; final QueryBuilder? recursiveQuery; final List? columns; const CteDefinition({ required this.name, required this.query, this.isRecursive = false, this.isMaterialized = false, this.isNotMaterialized = false, this.recursiveQuery, this.columns, }); void validate(CteConfiguration config) { SqlIdentifierEscaper.validateIdentifier(name, context: 'CTE name'); if (isRecursive && !config.supportsFeature(CteFeature.recursive)) { throw UnsupportedCteFeatureException( CteFeature.recursive, config.databaseType, name, ); } if (isMaterialized && !config.supportsFeature(CteFeature.materialized)) { throw UnsupportedCteFeatureException( CteFeature.materialized, config.databaseType, name, ); } if (isNotMaterialized && !config.supportsFeature(CteFeature.notMaterialized)) { throw UnsupportedCteFeatureException( CteFeature.notMaterialized, config.databaseType, name, ); } if (isRecursive && recursiveQuery == null) { throw InvalidCteConfigurationException( 'Recursive CTE must have recursive query', name, ); } if (isMaterialized && isNotMaterialized) { throw InvalidCteConfigurationException( 'CTE cannot be both materialized and not materialized', name, ); } if (isRecursive && (isMaterialized || isNotMaterialized)) { throw InvalidCteConfigurationException( 'Recursive CTE cannot have materialization options', name, ); } if (columns != null && columns!.isNotEmpty) { for (String column in columns!) { SqlIdentifierEscaper.validateIdentifier(column, context: 'Column name'); } Set uniqueColumns = columns!.map((c) => c.toLowerCase()).toSet(); if (uniqueColumns.length != columns!.length) { throw InvalidCteConfigurationException( 'CTE columns must be unique', name, ); } } } String toSql( IdentifierEscapingStrategy escapingStrategy, CteConfiguration config, ) { validate(config); String escapedName = escapingStrategy.escape(name); String sql = escapedName; if (columns != null && columns!.isNotEmpty) { List escapedColumns = columns! .map((col) => escapingStrategy.escape(col)) .toList(); sql += ' (${escapedColumns.join(', ')})'; } if (isRecursive && recursiveQuery != null) { sql += ' AS (${query.toSql()} UNION ALL ${recursiveQuery!.toSql()})'; } else if (isMaterialized && config.supportsFeature(CteFeature.materialized)) { sql += ' AS MATERIALIZED (${query.toSql()})'; } else if (isNotMaterialized && config.supportsFeature(CteFeature.notMaterialized)) { sql += ' AS NOT MATERIALIZED (${query.toSql()})'; } else { sql += ' AS (${query.toSql()})'; } return sql; } Map getBindings() { Map allBindings = {}; if (query is QueryBuilderAccessor) { Map queryBindings = (query as QueryBuilderAccessor) .accessBindings(); allBindings.addAll(queryBindings); } if (recursiveQuery != null && recursiveQuery is QueryBuilderAccessor) { Map recursiveBindings = (recursiveQuery as QueryBuilderAccessor).accessBindings(); for (var entry in recursiveBindings.entries) { String key = entry.key; if (allBindings.containsKey(key)) { int counter = 1; String newKey; do { newKey = '${key}_rec_$counter'; counter++; } while (allBindings.containsKey(newKey)); allBindings[newKey] = entry.value; } else { allBindings[key] = entry.value; } } } return allBindings; } @override int get hashCode { return Object.hash( name, query.hashCode, isRecursive, isMaterialized, isNotMaterialized, recursiveQuery?.hashCode, columns?.join(','), ); } @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is CteDefinition && other.name == name && other.query == query && other.isRecursive == isRecursive && other.isMaterialized == isMaterialized && other.isNotMaterialized == isNotMaterialized && other.recursiveQuery == recursiveQuery && _listEquals(other.columns, columns); } bool _listEquals(List? a, List? b) { if (a == null) return b == null; if (b == null || a.length != b.length) return false; for (int index = 0; index < a.length; index += 1) { if (a[index] != b[index]) return false; } return true; } @override String toString() { return 'CTE($name, recursive: $isRecursive, materialized: $isMaterialized)'; } } ================================================ FILE: lib/src/database/query_builder/_cte/_cte_exception.dart ================================================ class CteException implements Exception { final String message; final String? cteName; CteException(this.message, [this.cteName]); } ================================================ FILE: lib/src/database/query_builder/_cte/_cte_feature.dart ================================================ enum CteFeature { materialized, notMaterialized, recursive } ================================================ FILE: lib/src/database/query_builder/_cte/_database_type.dart ================================================ enum DatabaseType { postgresql, mysql, sqlite } ================================================ FILE: lib/src/database/query_builder/_cte/_duplicate_cte_name_exception.dart ================================================ import '_cte_exception.dart'; class DuplicateCteNameException extends CteException { DuplicateCteNameException(String name) : super('CTE with name "$name" already exists', name); } ================================================ FILE: lib/src/database/query_builder/_cte/_invalid_cte_configuration_exception.dart ================================================ import '_cte_exception.dart'; class InvalidCteConfigurationException extends CteException { InvalidCteConfigurationException(super.message, [super.cteName]); } ================================================ FILE: lib/src/database/query_builder/_cte/_sql_identifier_escaper.dart ================================================ import 'package:vania/src/exception/invalid_argument_exception.dart'; class SqlIdentifierEscaper { static const Set _reservedWords = { 'select', 'from', 'where', 'insert', 'update', 'delete', 'create', 'drop', 'alter', 'table', 'view', 'index', 'database', 'schema', 'order', 'group', 'having', 'limit', 'offset', 'join', 'inner', 'left', 'right', 'full', 'outer', 'cross', 'on', 'using', 'as', 'in', 'exists', 'count', 'sum', 'avg', 'min', 'max', 'case', 'when', 'then', 'else', 'end', 'and', 'or', 'like', 'between', 'true', 'false', 'is', 'begin', 'commit', 'rollback', 'with', 'recursive', 'union', 'all', 'distinct', }; static bool needsEscaping(String identifier) { if (identifier.isEmpty) return false; if (_hasInvalidPattern(identifier)) return true; if (_reservedWords.contains(identifier.toLowerCase())) return true; if (_looksLikeSqlKeyword(identifier)) return true; return false; } static bool _hasInvalidPattern(String identifier) { if (RegExp(r'^\d').hasMatch(identifier)) return true; if (!RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$').hasMatch(identifier)) return true; return false; } static bool _looksLikeSqlKeyword(String identifier) { final lower = identifier.toLowerCase(); if (identifier.length <= 10 && identifier == identifier.toUpperCase() && identifier.isNotEmpty && RegExp(r'^[A-Z]+$').hasMatch(identifier)) { if (RegExp( r'^(ID|URL|API|UUID|JSON|XML|HTML|CSS|JS)$', ).hasMatch(identifier)) { return false; } if (RegExp(r'(ID|NAME|CODE|TYPE|FLAG|DATA)$').hasMatch(identifier)) { return false; } return true; } if (RegExp( r'^(select|insert|update|delete|create|drop|alter|grant|revoke)', ).hasMatch(lower)) { return true; } if (RegExp( r'^(count|sum|avg|min|max|concat|substr|trim|upper|lower)$', ).hasMatch(lower)) { return true; } if (RegExp( r'^(now|today|current_date|current_time|current_timestamp)$', ).hasMatch(lower)) { return true; } if (RegExp(r'^(date_|time_)').hasMatch(lower) && !RegExp(r'_(at|on|by|for|from|to)$').hasMatch(lower)) { return true; } return false; } static void validateIdentifier( String identifier, { String context = 'Identifier', }) { if (identifier.trim().isEmpty) { throw InvalidArgumentException('$context cannot be empty or whitespace'); } if (identifier.length > 63) { throw InvalidArgumentException( '$context name is too long (max 63 characters): ${identifier.length}', ); } if (identifier.contains('0')) { throw InvalidArgumentException('$context cannot contain null character'); } } } ================================================ FILE: lib/src/database/query_builder/_cte/_standard_escaping_strategy.dart ================================================ import '_sql_identifier_escaper.dart'; abstract class IdentifierEscapingStrategy { String escape(String identifier); bool needsEscaping(String identifier); } class StandardEscapingStrategy implements IdentifierEscapingStrategy { final String quoteChar; final String? closingQuoteChar; const StandardEscapingStrategy(this.quoteChar, [this.closingQuoteChar]); @override String escape(String identifier) { if (!needsEscaping(identifier)) { return identifier; } String closing = closingQuoteChar ?? quoteChar; String escaped = identifier.replaceAll(quoteChar, '$quoteChar$quoteChar'); if (quoteChar == '[') { escaped = identifier.replaceAll(']', ']]'); return '[$escaped]'; } return '$quoteChar$escaped$closing'; } @override bool needsEscaping(String identifier) { return SqlIdentifierEscaper.needsEscaping(identifier); } } ================================================ FILE: lib/src/database/query_builder/_cte/_unsupported_cte_feature_exception.dart ================================================ import '_cte_exception.dart'; import '_cte_feature.dart'; import '_database_type.dart'; class UnsupportedCteFeatureException extends CteException { final CteFeature feature; final DatabaseType databaseType; UnsupportedCteFeatureException( this.feature, this.databaseType, [ String? cteName, ]) : super( 'Feature ${feature.name} is not supported in ${databaseType.name}', cteName, ); } ================================================ FILE: lib/src/database/query_builder/_cte_builder_impl.dart ================================================ import 'package:meta/meta.dart'; import '../../contract/database/query_builder/query_builder.dart'; import '../../exception/invalid_argument_exception.dart'; import '_cte/_cte_cache.dart'; import '_cte/_cte_configuration.dart'; import '_cte/_cte_definition.dart'; import '_cte/_cte_feature.dart'; import '_cte/_duplicate_cte_name_exception.dart'; import '_cte/_invalid_cte_configuration_exception.dart'; import '_cte/_sql_identifier_escaper.dart'; import '_cte/_standard_escaping_strategy.dart'; import '_cte/_unsupported_cte_feature_exception.dart'; mixin QueryBuilderAccessor on QueryBuilder { Map accessBindings() => getBindings(); } abstract mixin class CteBuilderImpl implements QueryBuilder { @protected final List _ctes = []; final Set _cteNames = {}; CteConfiguration _config = const CteConfiguration(); final CteCache _cache = CteCache(); late final IdentifierEscapingStrategy _escapingStrategy; void configureCte(CteConfiguration config) { if (_config != config) { _config = config; _cache.markDirty(); _escapingStrategy = StandardEscapingStrategy(config.quoteChar); } } CteConfiguration get cteConfiguration => _config; @override QueryBuilder withCte( String name, QueryBuilder subQuery, { List? columns, }) { _validateAndAddCte(name, subQuery, columns: columns); return this; } @override QueryBuilder withMultiple( Map ctes, { Map>? columnsMap, }) { if (ctes.isEmpty) { throw InvalidArgumentException('CTEs map cannot be empty'); } for (String name in ctes.keys) { SqlIdentifierEscaper.validateIdentifier(name, context: 'CTE name'); if (_cteNames.contains(name.toLowerCase())) { throw DuplicateCteNameException(name); } } for (var entry in ctes.entries) { List? columns = columnsMap?[entry.key]; _addCteDefinition(entry.key, entry.value, columns: columns); } return this; } @override QueryBuilder withRecursive( String name, QueryBuilder baseCase, QueryBuilder recursiveCase, { List? columns, }) { if (!_config.supportsFeature(CteFeature.recursive)) { throw UnsupportedCteFeatureException( CteFeature.recursive, _config.databaseType, name, ); } _validateAndAddCte( name, baseCase, isRecursive: true, recursiveQuery: recursiveCase, columns: columns, ); return this; } @override QueryBuilder withMaterialized( String name, QueryBuilder subQuery, { List? columns, }) { if (!_config.supportsFeature(CteFeature.materialized)) { throw UnsupportedCteFeatureException( CteFeature.materialized, _config.databaseType, name, ); } _validateAndAddCte(name, subQuery, isMaterialized: true, columns: columns); return this; } @override QueryBuilder withNotMaterialized( String name, QueryBuilder subQuery, { List? columns, }) { if (!_config.supportsFeature(CteFeature.notMaterialized)) { throw UnsupportedCteFeatureException( CteFeature.notMaterialized, _config.databaseType, name, ); } _validateAndAddCte( name, subQuery, isNotMaterialized: true, columns: columns, ); return this; } void _validateAndAddCte( String name, QueryBuilder query, { bool isRecursive = false, bool isMaterialized = false, bool isNotMaterialized = false, QueryBuilder? recursiveQuery, List? columns, }) { SqlIdentifierEscaper.validateIdentifier(name, context: 'CTE name'); String lowerName = name.toLowerCase(); if (_cteNames.contains(lowerName)) { throw DuplicateCteNameException(name); } _addCteDefinition( name, query, isRecursive: isRecursive, isMaterialized: isMaterialized, isNotMaterialized: isNotMaterialized, recursiveQuery: recursiveQuery, columns: columns, ); } void _addCteDefinition( String name, QueryBuilder query, { bool isRecursive = false, bool isMaterialized = false, bool isNotMaterialized = false, QueryBuilder? recursiveQuery, List? columns, }) { final cte = CteDefinition( name: name, query: query, isRecursive: isRecursive, isMaterialized: isMaterialized, isNotMaterialized: isNotMaterialized, recursiveQuery: recursiveQuery, columns: columns, ); cte.validate(_config); _ctes.add(cte); _cteNames.add(name.toLowerCase()); _cache.markDirty(); } @protected void validateAllCtes() { for (var cte in _ctes) { cte.validate(_config); } int recursiveCount = _ctes.where((cte) => cte.isRecursive).length; if (recursiveCount > _config.maxCteDepth) { throw InvalidCteConfigurationException( 'Too many recursive CTEs: $recursiveCount (max: ${_config.maxCteDepth})', ); } } @protected String buildWithClause() { if (_ctes.isEmpty) return ''; int currentHashCode = _calculateCteHashCode(); if (_config.enableCaching && !_cache.needsUpdate(currentHashCode)) { return _cache.cachedWithClause ?? ''; } validateAllCtes(); List cteStrings = []; bool hasRecursive = _ctes.any((cte) => cte.isRecursive); _escapingStrategy = StandardEscapingStrategy(_config.quoteChar); for (var cte in _ctes) { cteStrings.add(cte.toSql(_escapingStrategy, _config)); } String withClause = hasRecursive ? 'WITH RECURSIVE ' : 'WITH '; withClause += cteStrings.join(', '); if (_config.enableCaching) { Map bindings = _calculateCteBindings(); _cache.updateCache(withClause, bindings, currentHashCode); } return withClause; } @protected Map getCteBindings() { if (_ctes.isEmpty) return {}; int currentHashCode = _calculateCteHashCode(); if (_config.enableCaching && !_cache.needsUpdate(currentHashCode)) { return _cache.cachedBindings ?? {}; } return _calculateCteBindings(); } Map _calculateCteBindings() { Map allBindings = {}; for (int i = 0; i < _ctes.length; i++) { var cte = _ctes[i]; Map cteBindings = cte.getBindings(); for (var entry in cteBindings.entries) { String key = entry.key; if (allBindings.containsKey(key)) { int counter = 1; String newKey; do { newKey = '${key}_cte${i}_$counter'; counter++; } while (allBindings.containsKey(newKey)); allBindings[newKey] = entry.value; } else { allBindings[key] = entry.value; } } } return allBindings; } int _calculateCteHashCode() { return Object.hash( _ctes.map((cte) => cte.hashCode).toList(), _config.hashCode, ); } @protected void clearCtes() { _ctes.clear(); _cteNames.clear(); _cache.clear(); } @protected bool get hasCtes => _ctes.isNotEmpty; @protected List get cteNames => _cteNames.toList(); @protected int get cteCount => _ctes.length; @protected bool hasCte(String name) => _cteNames.contains(name.toLowerCase()); @protected bool removeCte(String name) { String lowerName = name.toLowerCase(); final index = _ctes.indexWhere( (cte) => cte.name.toLowerCase() == lowerName, ); if (index != -1) { _ctes.removeAt(index); _cteNames.remove(lowerName); _cache.markDirty(); return true; } return false; } @protected List> getCteInfo() { return _ctes .map( (cte) => { 'name': cte.name, 'isRecursive': cte.isRecursive, 'isMaterialized': cte.isMaterialized, 'isNotMaterialized': cte.isNotMaterialized, 'hasColumns': cte.columns != null && cte.columns!.isNotEmpty, 'columnCount': cte.columns?.length ?? 0, 'columns': cte.columns, 'hashCode': cte.hashCode, }, ) .toList(); } @protected Map getCacheStats() { return { 'isCacheEnabled': _config.enableCaching, 'isCacheValid': !_cache.needsUpdate(_calculateCteHashCode()), 'hasCachedWithClause': _cache.cachedWithClause != null, 'hasCachedBindings': _cache.cachedBindings != null, 'currentHashCode': _calculateCteHashCode(), }; } } ================================================ FILE: lib/src/database/query_builder/_delete_query_builder_impl.dart ================================================ import '../../contract/database/_connectors/_database_connection.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; abstract mixin class DeleteQueryBuilderImpl implements QueryBuilder { late DatabaseConnection conn; @override Future delete() async { try { final sql = "DELETE FROM $getTable${buildWhereClause()}"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } @override Future truncate({bool force = false}) async { try { String sql = "TRUNCATE TABLE $getTable"; if (force) { sql = "SET FOREIGN_KEY_CHECKS=0; $sql; SET FOREIGN_KEY_CHECKS=1;"; } conn = await getConnection(); await conn.execute(sql); return true; } catch (e) { rethrow; } } } ================================================ FILE: lib/src/database/query_builder/_insert_query_builder_impl.dart ================================================ import 'package:meta/meta.dart'; import '../../contract/database/_connectors/_database_connection.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; import '../../exception/invalid_argument_exception.dart'; abstract mixin class InsertQueryBuilderImpl implements QueryBuilder { late DatabaseConnection conn; @protected @override final Map bindings = {}; int _paramCounter = 0; String _nextParamName() { _paramCounter++; return 'p$_paramCounter'; } @override Future insert(Map values) async { try { if (values.isEmpty) { throw InvalidArgumentException( "Values map cannot be empty for insert operation.", ); } conn = await getConnection(); final columns = values.keys.toList(); final paramBindings = {}; // Create parameter placeholders final placeholders = values.keys .map((key) { final paramName = _nextParamName(); paramBindings[paramName] = values[key]; return ":$paramName"; }) .join(", "); final query = "INSERT INTO $getTable (${columns.join(', ')}) VALUES ($placeholders)"; await conn.insert(query, paramBindings); return true; } catch (e) { rethrow; } } @override Future insertGetId(Map values, [String? sequence]) async { try { if (values.isEmpty) { throw InvalidArgumentException( "Values map cannot be empty for insertGetId operation.", ); } conn = await getConnection(); final columns = values.keys.toList(); final paramBindings = {}; final placeholders = values.keys .map((key) { final paramName = _nextParamName(); paramBindings[paramName] = values[key]; return ":$paramName"; }) .join(", "); final query = "INSERT INTO $getTable (${columns.join(', ')}) VALUES ($placeholders)"; final id = await conn.insert(query, paramBindings); return id; } catch (e) { rethrow; } } @override Future insertMany(List> valuesList) async { try { if (valuesList.isEmpty) { throw InvalidArgumentException( "Values list cannot be empty for insertMany operation.", ); } // Ensure all maps have the same keys final firstItem = valuesList.first; final columns = firstItem.keys.toList(); for (var values in valuesList) { if (!_haveSameKeys(values, firstItem)) { throw InvalidArgumentException( "All items in the values list must have the same structure.", ); } } conn = await getConnection(); final paramBindings = {}; final valueGroups = []; // Create parameter placeholders for each row for (var values in valuesList) { final placeholders = columns .map((column) { final paramName = _nextParamName(); paramBindings[paramName] = values[column]; return ":$paramName"; }) .join(", "); valueGroups.add("($placeholders)"); } final query = "INSERT INTO $getTable (${columns.join(', ')}) VALUES ${valueGroups.join(', ')}"; await conn.execute(query, paramBindings); return true; } catch (e) { rethrow; } } bool _haveSameKeys(Map map1, Map map2) { if (map1.keys.length != map2.keys.length) return false; for (var key in map1.keys) { if (!map2.containsKey(key)) return false; } return true; } @override Future insertOrIgnore(Map values) async { try { if (values.isEmpty) { throw InvalidArgumentException( "Values map cannot be empty for insertOrIgnore operation.", ); } conn = await getConnection(); final columns = values.keys.toList(); final paramBindings = {}; final placeholders = values.keys .map((key) { final paramName = _nextParamName(); paramBindings[paramName] = values[key]; return ":$paramName"; }) .join(", "); final query = "INSERT IGNORE INTO $getTable (${columns.join(', ')}) VALUES ($placeholders)"; await conn.execute(query, paramBindings); return true; } catch (e) { rethrow; } } @override Future insertUsing(List columns, QueryBuilder subQuery) async { try { String cols = columns.join(", "); String subSql = subQuery.toRawSql(); String sql = "INSERT INTO $getTable ($cols) $subSql"; conn = await getConnection(); await conn.execute(sql); return true; } catch (e) { rethrow; } } @override Future upsert( Map values, List uniqueBy, [ Map? update, ]) async { try { if (values.isEmpty) { throw InvalidArgumentException( "Values map cannot be empty for upsert operation.", ); } conn = await getConnection(); final columns = values.keys.toList(); final paramBindings = {}; final placeholders = values.keys .map((key) { final paramName = _nextParamName(); paramBindings[paramName] = values[key]; return ":$paramName"; }) .join(", "); String sql = "INSERT INTO $getTable (${columns.join(', ')}) VALUES ($placeholders)"; if (update == null) { update = Map.from(values); for (var col in uniqueBy) { update.remove(col); } } if (update.isNotEmpty) { final updateClauses = update.entries .map((entry) { final paramName = _nextParamName(); paramBindings[paramName] = entry.value; return "${entry.key} = :$paramName"; }) .join(", "); sql += " ON DUPLICATE KEY UPDATE $updateClauses"; } await conn.execute(sql, paramBindings); return true; } catch (e) { rethrow; } } } ================================================ FILE: lib/src/database/query_builder/_join_clause_builder_impl.dart ================================================ import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; abstract mixin class JoinClauseBuilderImpl implements QueryBuilder { @override QueryBuilder crossJoin(String table, [List bindings = const []]) { String clause = "CROSS JOIN $table"; joins.add(clause); return this; } @override QueryBuilder join( String table, String firstColumn, [ String? operator, String? secondColumn, String type = 'INNER', bool where = false, ]) { String clause = "$type JOIN $table"; if (operator != null && secondColumn != null) { clause += " ON $firstColumn $operator $secondColumn"; } joins.add(clause); return this; } @override QueryBuilder joinSub( QueryBuilder subQuery, String as, String firstColumn, [ String? operator, String? secondColumn, String type = 'inner', ]) { String subSql = "(${subQuery.toSql()}) AS $as"; String clause = "$type JOIN $subSql"; if (operator != null && secondColumn != null) { clause += " ON $firstColumn $operator $secondColumn"; } joins.add(clause); return this; } @override QueryBuilder leftJoin( String table, String firstColumn, [ String? operator, String? secondColumn, bool where = false, ]) { return join(table, firstColumn, operator, secondColumn, "LEFT", where); } @override QueryBuilder leftJoinSub( QueryBuilder subQuery, String as, String firstColumn, [ String? operator, String? secondColumn, ]) { return joinSub(subQuery, as, firstColumn, operator, secondColumn, "LEFT"); } @override QueryBuilder rightJoin( String table, String firstColumn, [ String? operator, String? secondColumn, ]) { return join(table, firstColumn, operator, secondColumn, "RIGHT"); } } ================================================ FILE: lib/src/database/query_builder/_query_builder_impl.dart ================================================ import 'package:meta/meta.dart'; import '../../contract/database/query_builder/query_builder.dart'; import '../../exception/invalid_argument_exception.dart'; import '../../utils/helper.dart' show env; import '../_connection_manager.dart'; import '../monitoring/database_monitor.dart'; import '_bulk_operations_builder_impl.dart'; import '_cte_builder_impl.dart'; import '_delete_query_builder_impl.dart'; import '_insert_query_builder_impl.dart'; import '_join_clause_builder_impl.dart'; import '_where_clauses_builder_impl.dart'; import '_query_executor_builder_impl.dart'; import '_select_query_builder_impl.dart'; import '_union_clause_builder_impl.dart'; import '_update_query_builder_impl.dart'; import '_window_functions_builder_impl.dart'; class QueryBuilderImpl extends QueryBuilder with QueryExecutorBuilderImpl, InsertQueryBuilderImpl, UpdateQueryBuilderImpl, WhereClausesBuilderImpl, DeleteQueryBuilderImpl, SelectQueryBuilderImpl, JoinClauseBuilderImpl, UnionClauseBuilderImpl, WindowFunctionsBuilderImpl, BulkOperationsBuilderImpl, CteBuilderImpl { String _connectionName = env('DB_CONNECTION', ''); final List _orderBy = []; final List _groupBy = []; final List _having = []; String? _table; String? _tableAlias; int? _limit; int? _offset; int _paramCounter = 0; @override String get connectionName => _connectionName; set connectionName(String value) => _connectionName = value; @override String get getTable { String tableClause = (_table != null) ? "$_table" : ""; if (_tableAlias != null && _tableAlias!.isNotEmpty) { tableClause += " AS $_tableAlias"; } return tableClause; } @override RawExpression raw(value) => RawExpression(value); @override Future transaction( Future Function() action, [ String? conditionName, ]) => ConnectionManager().transaction(action, conditionName); @override Stream alerts() => ConnectionManager().alerts; @override Map getPerformanceStats() => ConnectionManager().getPerformanceStats(); @protected @override String build({String? aggregateFunction, String? aggregateColumn}) { String sql = ''; if (selectColumns.length > 1) { selectColumns.remove('*'); } String withClause = buildWithClause(); if (withClause.isNotEmpty) { sql = '$withClause '; } if (getTable.isNotEmpty) { if (aggregateFunction != null && aggregateColumn != null) { sql += "SELECT $aggregateFunction($aggregateColumn) FROM $getTable"; } else { sql += "SELECT ${selectColumns.isEmpty ? "*" : selectColumns.join(", ")} FROM $getTable"; } if (joins.isNotEmpty) { sql += " ${joins.join(" ")}"; } sql += conditions.isNotEmpty ? " WHERE ${conditions.join(" ")}" : ""; if (unions.isNotEmpty) { sql += " ${unions.join(" ")}"; } if (aggregateFunction == null && aggregateColumn == null) { if (_groupBy.isNotEmpty) { sql += " GROUP BY ${_groupBy.join(", ")}"; } if (_having.isNotEmpty) { sql += " HAVING ${_having.join(" ")}"; } if (_orderBy.isNotEmpty) { sql += " ORDER BY ${_orderBy.join(", ")}"; } sql += (_limit != null) ? " LIMIT $_limit" : ""; sql += (_offset != null) ? " OFFSET $_offset" : ""; } } else if (conditions.isNotEmpty) { sql += conditions.join(" "); } else { sql += ''; } return sql.trim(); } @override QueryBuilder connection([String? connection]) { connectionName = connection ?? _connectionName; return this; } @override QueryBuilder groupBy(List groups) { _groupBy.addAll(groups); return this; } @override QueryBuilder having( String column, [ String? operator, dynamic value, String boolean = 'and', ]) { String clause; if (operator != null && value != null) { final paramName = _nextParamName(); bindings[paramName] = value; clause = "$column $operator :$paramName"; } else { clause = column; } if (_having.isEmpty) { _having.add(clause); } else { _having.add(" $boolean $clause"); } return this; } @override QueryBuilder havingBetween( String column, List values, { String boolean = 'and', bool not = false, }) { if (values.length < 2) { throw InvalidArgumentException( 'The list of values must contain at least two items.', ); } final paramName1 = _nextParamName(); final paramName2 = _nextParamName(); bindings[paramName1] = values[0]; bindings[paramName2] = values[1]; String clause = "$column ${not ? "NOT BETWEEN" : "BETWEEN"} :$paramName1 AND :$paramName2"; if (_having.isEmpty) { _having.add(clause); } else { _having.add(" $boolean $clause"); } return this; } @override QueryBuilder inRandomOrder([dynamic seed]) { if (seed != null) { _orderBy.add("RAND($seed)"); } else { _orderBy.add("RAND()"); } return this; } @override QueryBuilder latest([String column = 'created_at']) { return orderByDesc(column); } @override QueryBuilder limit(int value) { _limit = value; return this; } @override QueryBuilder offset(int value) { _offset = value; return this; } @override QueryBuilder orderBy(String column, [String direction = 'ASC']) { _orderBy.add("$column $direction"); return this; } @override QueryBuilder orderByAsc(String column) { return orderBy(column, "ASC"); } @override QueryBuilder orderByDesc(String column) { return orderBy(column, "DESC"); } @override QueryBuilder reorder([String? column, String? direction]) { _orderBy.clear(); if (column != null) { _orderBy.add("$column ${direction ?? 'asc'}"); } return this; } @override QueryBuilder table(String table, [String? as]) { _table = table; _tableAlias = as; return this; } @override QueryBuilder skip(int value) => offset(value); @override QueryBuilder take(int value) => limit(value); @override String toSql() => build(); @override String toRawSql() { String sql = build(); final bindings = getBindings(); bindings.forEach((key, value) { String placeholder = ':$key'; String formattedValue = _formatValueForRawSql(value); sql = sql.replaceAll(placeholder, formattedValue); }); return sql; } String _formatValueForRawSql(dynamic value) { if (value == null) { return 'NULL'; } else if (value is RawExpression) { return value.toString(); } else if (value is String) { return "'${value.replaceAll("'", "''")}'"; } else if (value is num) { return value.toString(); } else if (value is bool) { return value.toString(); } else if (value is DateTime) { return "'${value.toIso8601String()}'"; } else if (value is List) { return '(${value.map(_formatValueForRawSql).join(', ')})'; } else { return "'${value.toString().replaceAll("'", "''")}'"; } } @override Map getBindings() { Map allBindings = {}; allBindings.addAll((this as WhereClausesBuilderImpl).bindings); allBindings.addAll(getCteBindings()); (this as WhereClausesBuilderImpl).paramCounter = 0; return allBindings; } String _nextParamName() { _paramCounter++; return 'p$_paramCounter'; } } ================================================ FILE: lib/src/database/query_builder/_query_executor_builder_impl.dart ================================================ import 'package:vania/src/utils/request_helper.dart' show getParam; import '../../contract/database/_connectors/_database_connection.dart'; import '../../exception/invalid_argument_exception.dart'; import '../../contract/database/query_builder/query_builder.dart' show PaginatedResult, QueryBuilder; abstract mixin class QueryExecutorBuilderImpl implements QueryBuilder { late DatabaseConnection conn; @override Future avg(String column) async { try { String sql = build(aggregateFunction: "AVG", aggregateColumn: column); final bindings = getBindings(); conn = await getConnection(); var result = await conn.select(sql, bindings); return num.tryParse(result.first.values.first.toString()) ?? 0; } catch (e) { rethrow; } } @override Future chunk( int chunk, void Function(List> data) callback, ) async { try { int offset = 0; while (true) { limit(chunk).offset(offset); final result = await get(); if (result.isEmpty) { break; } callback(result); offset += chunk; if (result.length < chunk) { break; } } } catch (e) { rethrow; } } @override Future chunkById( int chunk, void Function(List> data) callback, [ String column = 'id', ]) async { try { int lastId = 0; while (true) { whereGreaterThan(column, lastId).orderByAsc(column).limit(chunk); final result = await get(); if (result.isEmpty) { break; } callback(result); lastId = int.parse(result.last[column].toString()); if (result.length < chunk) { break; } } } catch (e) { rethrow; } } @override Future count([String columns = '*']) async { try { final bindings = getBindings(); String sql = build(aggregateFunction: "COUNT", aggregateColumn: columns); conn = await getConnection(); var result = await conn.select(sql, bindings); return int.tryParse(result.first.values.first.toString()) ?? 0; } catch (e) { rethrow; } } @override Future doesntExist() async { try { String sql = "SELECT NOT EXISTS(SELECT 1 FROM $getTable"; if (conditions.isNotEmpty) { sql += " WHERE ${conditions.join('')}"; } sql += ") as `exists`"; final bindings = getBindings(); var result = await dbConnection!.select(sql, bindings); return (int.tryParse(result.first["exists"].toString()) == 1); } catch (e) { rethrow; } } @override Future each(void Function(Map q) callback) async { var results = await get(); for (var row in results) { callback(row); } } @override Future exists() async { try { String sql = "SELECT EXISTS(SELECT 1 FROM $getTable"; if (conditions.isNotEmpty) { sql += " WHERE ${conditions.join('')}"; } sql += ") as `exists`"; final bindings = getBindings(); var result = await dbConnection!.select(sql, bindings); return (int.tryParse(result.first["exists"].toString()) == 1); } catch (e) { rethrow; } } @override Future?> find( dynamic id, { String byColumnName = 'id', List columns = const [], }) async { try { String sql = whereEqualTo('$getTable.$byColumnName', id).limit(1).toSql(); if (columns.isNotEmpty) { for (String column in columns) { selectColumns.remove(column); } selectColumns.addAll(columns); } final bindings = getBindings(); final result = await dbConnection!.select(sql, bindings); if (result.isEmpty) { return null; } return result.first; } catch (e) { rethrow; } } @override Future?> findOrFail( id, { String byColumnName = 'id', List columns = const [], }) async { var result = await find(id, byColumnName: byColumnName, columns: columns); if (result == null) { throw InvalidArgumentException("Record with id $id not found."); } return result; } @override Future?> first([List columns = const []]) async { try { if (columns.isNotEmpty) { for (String column in columns) { selectColumns.remove(column); } selectColumns.addAll(columns); } final bindings = getBindings(); String sql = limit(1).toSql(); conn = await getConnection(); final result = await conn.select(sql, bindings); if (result.isEmpty) { return null; } return result.first; } catch (e) { rethrow; } } @override Future?> firstOrFail([ List columns = const [], ]) async { var result = await first(columns); if (result == null) { throw InvalidArgumentException("No records found."); } return result; } @override Future?> firstWhere( String column, [ String? operator = '=', value, List columns = const [], ]) async { if (value == null) { throw InvalidArgumentException( "Invalid input: Value cannot be null. A valid value must be provided for the firstWhere method.", ); } where(column, operator ?? '=', value); return await first(columns); } @override Future>> get([ List columns = const [], ]) async { try { final bindings = getBindings(); if (columns.isNotEmpty) { for (String column in columns) { selectColumns.remove(column); } selectColumns.addAll(columns); } final sql = toSql(); conn = await getConnection(); return await conn.select(sql, bindings); } catch (e) { rethrow; } } @override Stream>> lazy([ int chunk = 100, String column = 'id', ]) async* { int offset = 0; while (true) { orderByAsc(column).limit(chunk).offset(offset); final result = await get(); if (result.isEmpty) { break; } yield result; offset += chunk; if (result.length < chunk) { break; } } } @override Stream> cursor([int chunk = 1000]) async* { try { int offset = 0; while (true) { limit(chunk).offset(offset); final result = await get(); if (result.isEmpty) { break; } for (final row in result) { yield row; } offset += chunk; if (result.length < chunk) { break; } } } catch (e) { rethrow; } } @override Future max(String column) async { try { final bindings = getBindings(); String sql = build(aggregateFunction: "MAX", aggregateColumn: column); conn = await getConnection(); var result = await conn.select(sql, bindings); return result.first.values.first; } catch (e) { rethrow; } } @override Future min(String column) async { try { final bindings = getBindings(); String sql = build(aggregateFunction: "MIN", aggregateColumn: column); conn = await getConnection(); var result = await conn.select(sql, bindings); return result.first.values.first; } catch (e) { rethrow; } } @override Future> paginate({ int perPage = 15, List columns = const [], String? pageName, int? page, }) async { int currentPage = page ?? getParam('page', 1)!; int total = await count(); final lastPage = (total / perPage).ceil(); final offset = (currentPage - 1) * perPage; final pageData = await take(perPage).skip(offset).get(columns); final isFirst = currentPage == 1; final isLast = currentPage == lastPage; final hasMore = currentPage < lastPage; return PaginatedResult( data: pageData, currentPage: currentPage, perPage: perPage, total: total, lastPage: lastPage, isFirst: isFirst, isLast: isLast, hasMore: hasMore, ).toMap(); } @override Future pluck(String column, [String? key]) async { var results = await get(); if (key == null) { return results.map((row) => row[column]).toList(); } else { Map resultMap = {}; for (var row in results) { resultMap[row[key]] = row[column]; } return resultMap; } } @override Future> simplePaginate([ int perPage = 15, List columns = const [], String? pageName, int? page, ]) async { int currentPage = page ?? getParam('page', 1)!; int total = await count(); final lastPage = (total / perPage).ceil(); final offset = (currentPage - 1) * perPage; final pageData = await take(perPage).skip(offset).get(columns); return { 'data': pageData, 'current_page': currentPage, 'per_page': perPage, 'total': total, 'last_page': lastPage, }; } @override Future sum(String column) async { try { final bindings = getBindings(); String sql = build(aggregateFunction: "SUM", aggregateColumn: column); conn = await getConnection(); var result = await conn.select(sql, bindings); return num.tryParse(result.first.values.first.toString()) ?? 0; } catch (e) { rethrow; } } @override Future value(String column) async { final response = await first(); if (response != null && response.containsKey(column)) { return response[column]; } return null; } } ================================================ FILE: lib/src/database/query_builder/_select_query_builder_impl.dart ================================================ import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; abstract mixin class SelectQueryBuilderImpl implements QueryBuilder { @override QueryBuilder addSelect(List columns) { selectColumns.addAll(columns); return this; } @override QueryBuilder select([List columns = const ['*']]) { selectColumns = List.from(columns); return this; } @override QueryBuilder selectRaw(String query, [List bindings = const []]) { for (var i = 0; i < bindings.length; i++) { final paramName = 'p${i + 1}'; this.bindings[paramName] = bindings[i]; query = query.replaceFirst('?', ':$paramName'); } selectColumns.add(query); return this; } @override QueryBuilder selectSub(QueryBuilder subQuery, String as) { String sub = "(${subQuery.toRawSql()}) AS $as"; selectColumns.add(sub); return this; } } ================================================ FILE: lib/src/database/query_builder/_union_clause_builder_impl.dart ================================================ import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; abstract mixin class UnionClauseBuilderImpl implements QueryBuilder { @override QueryBuilder union(QueryBuilder query) { unions.add("UNION ${toSql()}"); return this; } @override QueryBuilder unionAll(QueryBuilder query) { unions.add("UNION ALL ${toSql()}"); return this; } } ================================================ FILE: lib/src/database/query_builder/_update_query_builder_impl.dart ================================================ import '../../contract/database/_connectors/_database_connection.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; import '../../exception/invalid_argument_exception.dart'; abstract mixin class UpdateQueryBuilderImpl implements QueryBuilder { late DatabaseConnection conn; @override Future update(Map values) async { try { if (values.isEmpty) { throw InvalidArgumentException('Update values cannot be empty'); } List setStatements = []; for (var entry in values.entries) { final paramName = 'p${entry.key}'; bindings[paramName] = entry.value; setStatements.add("${entry.key} = :$paramName"); } String sql = "UPDATE $getTable${buildJoins()} SET ${setStatements.join(", ")}${buildWhereClause()}"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } @override Future updateMany( List> updates, String column, ) async { try { if (updates.isEmpty) return false; Set columns = {}; for (var row in updates) { columns.addAll(row.keys.where((key) => key != column)); } List setClauses = []; var caseCounter = 0; for (var col in columns) { List cases = []; for (var row in updates) { if (row.containsKey(col)) { final keyParamName = 'p$caseCounter'; final valueParamName = 'p$caseCounter'; bindings[keyParamName] = row[column]; bindings[valueParamName] = row[col]; cases.add("WHEN $column = :$keyParamName THEN :$valueParamName"); caseCounter++; } } setClauses.add("$col = CASE ${cases.join(" ")} ELSE $col END"); } List whereValues = []; for (var i = 0; i < updates.length; i++) { final paramName = 'p$i'; bindings[paramName] = updates[i][column]; whereValues.add(":$paramName"); } String sql = "UPDATE $getTable${buildJoins()} SET ${setClauses.join(", ")} WHERE $column IN (${whereValues.join(", ")})"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } @override Future updateOrInsert( Map search, Map update, ) async { Map data = {} ..addAll(search) ..addAll(update); return upsert(data, search.keys.toList(), update); } @override Future increment( String column, [ int amount = 1, Map extra = const {}, ]) async { try { final paramName = 'pamount'; bindings[paramName] = amount; String setClause = "$column = $column + :$paramName"; if (extra.isNotEmpty) { var extraCounter = 0; List extraClauses = []; for (var entry in extra.entries) { final extraParamName = 'p${extraCounter++}'; bindings[extraParamName] = entry.value; extraClauses.add("${entry.key} = :$extraParamName"); } setClause += ", ${extraClauses.join(", ")}"; } String sql = "UPDATE $getTable${buildJoins()} SET $setClause${buildWhereClause()}"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } @override Future decrement( String column, [ int amount = 1, Map extra = const {}, ]) async { try { final paramName = 'pamount'; bindings[paramName] = amount; String setClause = "$column = $column - :$paramName"; if (extra.isNotEmpty) { var extraCounter = 0; List extraClauses = []; for (var entry in extra.entries) { final extraParamName = 'p${extraCounter++}'; bindings[extraParamName] = entry.value; extraClauses.add("${entry.key} = :$extraParamName"); } setClause += ", ${extraClauses.join(", ")}"; } String sql = "UPDATE $getTable${buildJoins()} SET $setClause${buildWhereClause()}"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } @override Future incrementEach( Map increments, [ Map extra = const {}, ]) async { try { List setClauses = []; var counter = 0; for (var entry in increments.entries) { final paramName = 'p${counter++}'; bindings[paramName] = entry.value; setClauses.add("${entry.key} = ${entry.key} + :$paramName"); } if (extra.isNotEmpty) { for (var entry in extra.entries) { final paramName = 'p${counter++}'; bindings[paramName] = entry.value; setClauses.add("${entry.key} = :$paramName"); } } String sql = "UPDATE $getTable${buildJoins()} SET ${setClauses.join(", ")}${buildWhereClause()}"; conn = await getConnection(); return await conn.execute(sql, bindings); } catch (e) { rethrow; } } } ================================================ FILE: lib/src/database/query_builder/_where_clauses_builder_impl.dart ================================================ import '../../exception/invalid_argument_exception.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder, QueryCallback; import '../_database_utils/_singularize.dart'; import '_query_builder_impl.dart'; int _paramCounter = 0; abstract mixin class WhereClausesBuilderImpl implements QueryBuilder { /// Valid SQL comparison operators to prevent SQL injection. /// Only these operators are allowed in where clauses. static const Set _validOperators = { '=', '<>', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'ILIKE', // PostgreSQL case-insensitive LIKE 'NOT ILIKE', 'REGEXP', 'NOT REGEXP', 'RLIKE', // MySQL alias for REGEXP 'SIMILAR TO', // PostgreSQL }; set paramCounter(int paramN) { _paramCounter = paramN; } /// Returns the current parameter counter value. /// Useful for synchronizing nested queries. int get currentParamCounter => _paramCounter; String _nextParamName() { _paramCounter++; return 'p$_paramCounter'; } /// Validates that the given operator is a valid SQL comparison operator. /// Throws [InvalidArgumentException] if the operator is not valid. /// This prevents SQL injection attacks through malicious operators. void _validateOperator(String operator) { if (!_validOperators.contains(operator.toUpperCase())) { throw InvalidArgumentException( 'Invalid SQL operator: "$operator". ' 'Allowed operators: ${_validOperators.join(", ")}', ); } } @override String buildWhereClause() { return conditions.isNotEmpty ? " WHERE ${conditions.join(" ")}" : ""; } @override String build({String? aggregateFunction, String? aggregateColumn}) { throw UnimplementedError( 'build() should be implemented by the concrete class', ); } @override Map getBindings() { return bindings; } @override QueryBuilder orWhere( dynamic condition, [ String operator = '=', dynamic value, String boolean = 'and', ]) { if (condition is String) { _validateOperator(operator); final paramName = _nextParamName(); bindings[paramName] = value; _appendCondition("$condition $operator :$paramName", isOr: true); } else if (condition is QueryCallback) { QueryBuilderImpl nested = QueryBuilderImpl() ..paramCounter = _paramCounter; condition(nested); _appendCondition("(${nested.toSql()})", isOr: true); bindings.addAll(nested.getBindings()); } else { throw InvalidArgumentException( 'Invalid argument type for condition. Expected either a String or a QueryBuilder instance', ); } return this; } @override QueryBuilder orWhereBetween(String column, List values, {bool not = false}) { _appendCondition(_createBetweenCondition(column, values, not), isOr: true); return this; } @override QueryBuilder orWhereColumn( String first, String operator, String secondColumn, ) { String condition = "$first $operator $secondColumn"; _appendCondition(condition, isOr: true); return this; } @override QueryBuilder orWhereDate(String column, String operator, dynamic value) { _appendCondition( _createDateCondition(column, operator, value, "DATE"), isOr: true, ); return this; } @override QueryBuilder orWhereDay(String column, String operator, dynamic value) { _appendCondition( _createDateCondition(column, operator, value, "DAY"), isOr: true, ); return this; } @override QueryBuilder orWhereExists(QueryCallback callback, {bool not = false}) { QueryBuilder subQuery = QueryBuilderImpl(); callback(subQuery); String condition = "${not ? 'NOT EXISTS' : 'EXISTS'} (${subQuery.toSql()})"; _appendCondition(condition, isOr: true); bindings.addAll(subQuery.getBindings()); return this; } @override QueryBuilder orWhereFullText( dynamic columns, dynamic query, [ Map options = const {}, ]) { _appendCondition( _createFullTextCondition(columns, query, options), isOr: true, ); return this; } @override QueryBuilder orWhereHour(String column, String operator, dynamic value) { _appendCondition(_createHourCondition(column, operator, value), isOr: true); return this; } @override QueryBuilder orWhereIn(String column, List values, {bool not = false}) { _appendCondition(_createInCondition(column, values, not), isOr: true); return this; } @override QueryBuilder orWhereJsonContains( String column, dynamic value, { bool not = false, }) { _appendCondition( _createJsonContainsCondition(column, value, not), isOr: true, ); return this; } @override QueryBuilder orWhereJsonDoesntContain(String column, dynamic value) { _appendCondition( _createJsonContainsCondition(column, value, true), isOr: true, ); return this; } @override QueryBuilder orWhereJsonLength( String column, String operator, dynamic value, ) { _appendCondition( _createJsonLengthCondition(column, operator, value), isOr: true, ); return this; } @override QueryBuilder orWhereLike( String column, dynamic value, { bool caseSensitive = false, }) { _appendCondition( _createLikeCondition( column, value, not: false, caseSensitive: caseSensitive, ), isOr: true, ); return this; } @override QueryBuilder orWhereMonth(String column, String operator, dynamic value) { _appendCondition( _createDateCondition(column, operator, value, "MONTH"), isOr: true, ); return this; } @override QueryBuilder orWhereNotBetween(String column, List values) { _appendCondition(_createBetweenCondition(column, values, true), isOr: true); return this; } @override QueryBuilder orWhereNotExists(QueryCallback callback) { return orWhereExists(callback, not: true); } @override QueryBuilder orWhereNotIn(String column, dynamic values) { _appendCondition(_createInCondition(column, values, true), isOr: true); return this; } @override QueryBuilder orWhereNotLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }) { _appendCondition( _createLikeCondition( column, value, not: true, caseSensitive: caseSensitive, ), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder orWhereNotNull(String column) { _appendCondition(_createNullCondition(column, true), isOr: true); return this; } @override QueryBuilder orWhereNull(String column) { _appendCondition(_createNullCondition(column, false), isOr: true); return this; } @override QueryBuilder orWhereRaw(String sql, [List rawBindings = const []]) { String processedSQL = _processRawSQL(sql, rawBindings); _appendCondition(processedSQL, isOr: true); return this; } @override QueryBuilder orWhereRowValues( List columns, String operator, List values, ) { _appendCondition( _createRowValuesCondition(columns, operator, values), isOr: true, ); return this; } @override QueryBuilder orWhereTime(String column, String operator, dynamic value) { _appendCondition( _createDateCondition(column, operator, value, "TIME"), isOr: true, ); return this; } @override QueryBuilder orWhereYear(String column, String operator, dynamic value) { _appendCondition( _createDateCondition(column, operator, value, "YEAR"), isOr: true, ); return this; } @override QueryBuilder where( dynamic condition, [ String operator = '=', dynamic value, String boolean = 'and', ]) { if (condition is String) { _validateOperator(operator); final paramName = _nextParamName(); bindings[paramName] = value; _appendCondition( "$condition $operator :$paramName", isOr: (boolean.toLowerCase() == 'or'), ); } else if (condition is QueryCallback) { QueryBuilderImpl nested = QueryBuilderImpl() ..paramCounter = _paramCounter; condition(nested); _appendCondition( "(${nested.toSql()})", isOr: (boolean.toLowerCase() == 'or'), ); bindings.addAll(nested.getBindings()); } else { throw InvalidArgumentException( 'Invalid argument type for condition. Expected either a String or a QueryBuilder instance', ); } return this; } @override QueryBuilder whereAfterToday(String column, {String boolean = 'and'}) { _appendCondition( "DATE($column) > CURDATE()", isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereAll( String column, List values, { String boolean = 'and', }) { if (values.isEmpty) { throw InvalidArgumentException("The list of values must not be empty."); } List conditions = []; for (var value in values) { final paramName = _nextParamName(); bindings[paramName] = value; conditions.add("$column = :$paramName"); } _appendCondition( conditions.join(" AND "), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereAny( String column, List values, { String boolean = 'and', }) { if (values.isEmpty) { throw InvalidArgumentException("The list of values must not be empty."); } _appendCondition( _createInCondition(column, values, false), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereBeforeToday(String column, {String boolean = 'and'}) { _appendCondition( "DATE($column) < CURDATE()", isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereBetween( String column, List values, { String boolean = 'and', bool not = false, }) { _appendCondition( _createBetweenCondition(column, values, not), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereBetweenColumns( String column, List columns, { String boolean = 'and', }) { _appendCondition( _createBetweenColumnsCondition(column, columns), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereColumn( String firstColumn, String operator, String secondColumn, [ String boolean = 'and', ]) { String condition = "$firstColumn $operator $secondColumn"; _appendCondition(condition, isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereDate( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createDateCondition(column, operator, value, "DATE"), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereDay( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createDateCondition(column, operator, value, "DAY"), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereEqualTo(condition, [value, String boolean = 'and']) => where(condition, '=', value, boolean); @override QueryBuilder whereExists( QueryCallback callback, { String boolean = 'and', bool not = false, }) { QueryBuilder subQuery = QueryBuilderImpl(); callback(subQuery); String condition = "${not ? 'NOT EXISTS' : 'EXISTS'} (${subQuery.toSql()})"; _appendCondition(condition, isOr: (boolean.toLowerCase() == 'or')); bindings.addAll(subQuery.getBindings()); return this; } @override QueryBuilder whereFullText( dynamic columns, dynamic query, [ Map options = const {}, ]) { _appendCondition( _createFullTextCondition(columns, query, options), isOr: false, ); return this; } @override QueryBuilder whereFuture(String column, {String boolean = 'and'}) { _appendCondition("$column > NOW()", isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereGreaterThan(condition, [value, String boolean = 'and']) => where(condition, '>', value, boolean); @override QueryBuilder whereGreaterThanOrEqualTo( condition, [ value, String boolean = 'and', ]) => where(condition, '>=', value, boolean); @override QueryBuilder whereHour( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createHourCondition(column, operator, value), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereIn( String column, List values, { String boolean = 'and', bool not = false, }) { _appendCondition( _createInCondition(column, values, not), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereJsonContains( String column, dynamic value, { String boolean = 'and', bool not = false, }) { _appendCondition( _createJsonContainsCondition(column, value, not), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereJsonDoesntContain( String column, dynamic value, { String boolean = 'and', }) { _appendCondition( _createJsonContainsCondition(column, value, true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereJsonLength( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createJsonLengthCondition(column, operator, value), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereLessThan(condition, [value, String boolean = 'and']) => where(condition, '<', value, boolean); @override QueryBuilder whereLessThanOrEqualTo( condition, [ value, String boolean = 'and', ]) => where(condition, '<=', value, boolean); @override QueryBuilder whereLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }) { _appendCondition( _createLikeCondition( column, value, not: false, caseSensitive: caseSensitive, ), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereMonth( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createDateCondition(column, operator, value, "MONTH"), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNone( String column, List values, { String boolean = 'and', }) { if (values.isEmpty) { throw InvalidArgumentException("The list of values must not be empty."); } _appendCondition( _createInCondition(column, values, true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNotBetween( String column, List values, { String boolean = 'and', }) { _appendCondition( _createBetweenCondition(column, values, true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNotBetweenColumns( String column, List columns, { String boolean = 'and', }) { _appendCondition( _createBetweenColumnsCondition(column, columns, not: true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNotEqualTo(condition, [value, String boolean = 'and']) => where(condition, '<>', value, boolean); @override QueryBuilder whereNotExists( QueryCallback callback, { String boolean = 'and', }) { return whereExists(callback, boolean: boolean, not: true); } @override QueryBuilder whereNotIn( String column, dynamic values, { String boolean = 'and', }) { _appendCondition( _createInCondition(column, values, true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNotLike( String column, dynamic value, { bool caseSensitive = false, String boolean = 'and', }) { _appendCondition( _createLikeCondition( column, value, not: true, caseSensitive: caseSensitive, ), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNotNull(String column, {String boolean = 'and'}) { _appendCondition( _createNullCondition(column, true), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereNowOrFuture(String column, {String boolean = 'and'}) { _appendCondition("$column >= NOW()", isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereNowOrPast(String column, {String boolean = 'and'}) { _appendCondition("$column <= NOW()", isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereNull( String column, { String boolean = 'and', bool not = false, }) { _appendCondition( _createNullCondition(column, not), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder wherePast(String column, {String boolean = 'and'}) { _appendCondition("$column < NOW()", isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereRaw( String sql, [ List rawBindings = const [], String boolean = 'and', ]) { String processedSQL = _processRawSQL(sql, rawBindings); _appendCondition(processedSQL, isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder whereRowValues( List columns, String operator, List values, { String boolean = 'and', }) { _appendCondition( _createRowValuesCondition(columns, operator, values), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereTime( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createDateCondition(column, operator, value, "TIME"), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereToday(String column, {String boolean = 'and'}) { _appendCondition( "DATE($column) = CURDATE()", isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereTodayOrAfter(String column, {String boolean = 'and'}) { _appendCondition( "DATE($column) >= CURDATE()", isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereTodayOrBefore(String column, {String boolean = 'and'}) { _appendCondition( "DATE($column) <= CURDATE()", isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereYear( String column, String operator, dynamic value, { String boolean = 'and', }) { _appendCondition( _createDateCondition(column, operator, value, "YEAR"), isOr: (boolean.toLowerCase() == 'or'), ); return this; } @override QueryBuilder whereHas( String relation, QueryCallback callback, { String boolean = 'and', }) { String condition = _createRelationshipCondition(relation, callback, false); _appendCondition(condition, isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder orWhereHas(String relation, QueryCallback callback) { String condition = _createRelationshipCondition(relation, callback, false); _appendCondition(condition, isOr: true); return this; } @override QueryBuilder whereDoesntHave( String relation, QueryCallback callback, { String boolean = 'and', }) { String condition = _createRelationshipCondition(relation, callback, true); _appendCondition(condition, isOr: (boolean.toLowerCase() == 'or')); return this; } @override QueryBuilder orWhereDoesntHave(String relation, QueryCallback callback) { String condition = _createRelationshipCondition(relation, callback, true); _appendCondition(condition, isOr: true); return this; } @override QueryBuilder withSoftDeletes([String column = 'deleted_at']) => whereNull(column); String _createRelationshipCondition( String relation, QueryCallback callback, bool not, ) { if (relation.isEmpty) { throw InvalidArgumentException( 'Relation name cannot be empty for relationship queries.', ); } QueryBuilder subQuery = QueryBuilderImpl(); subQuery.table(relation); callback(subQuery); String currentTable = getTable.split(' ').first; String foreignKey = '${Singularize.make(currentTable)}_id'; subQuery.whereColumn('$relation.$foreignKey', '=', '$currentTable.id'); String subQuerySQL = subQuery.toSql(); bindings.addAll(subQuery.getBindings()); String existsClause = not ? 'NOT EXISTS' : 'EXISTS'; return "$existsClause (SELECT 1 FROM $relation WHERE $relation.$foreignKey = $currentTable.id AND (${_extractWhereFromSubQuery(subQuerySQL)}))"; } String _extractWhereFromSubQuery(String sql) { int whereIndex = sql.indexOf('WHERE'); if (whereIndex != -1) { return sql.substring(whereIndex + 5).trim(); } return '1=1'; } void _appendCondition(String condition, {bool isOr = false}) { if (conditions.isEmpty) { conditions.add(condition); } else { String joiner = isOr ? "OR" : "AND"; conditions.add("$joiner $condition"); } } String _createBetweenColumnsCondition( String column, List columns, { bool not = false, }) { if (columns.length < 2) { throw InvalidArgumentException( 'At least two columns must be provided for whereBetweenColumns.', ); } String op = not ? "NOT BETWEEN" : "BETWEEN"; return "$column $op ${columns[0]} AND ${columns[1]}"; } String _createBetweenCondition(String column, List values, bool not) { if (values.length < 2) { throw InvalidArgumentException( 'The list of values must contain at least two items.', ); } final paramName1 = _nextParamName(); final paramName2 = _nextParamName(); bindings[paramName1] = values[0]; bindings[paramName2] = values[1]; return "$column ${not ? 'NOT BETWEEN' : 'BETWEEN'} :$paramName1 AND :$paramName2"; } String _createDateCondition( String column, String operator, dynamic value, String function, ) { if (value == null) { throw InvalidArgumentException( 'The value for the function $function must not be null.', ); } final paramName = _nextParamName(); bindings[paramName] = value; return "$function($column) $operator :$paramName"; } String _createFullTextCondition( dynamic columns, dynamic query, Map options, ) { String colStr; if (columns is List) { colStr = columns.join(", "); } else { colStr = columns.toString(); } final paramName = _nextParamName(); bindings[paramName] = query; String mode = ""; if (options.containsKey('mode')) { mode = " IN ${options['mode']} MODE"; } return "MATCH($colStr) AGAINST(:$paramName$mode)"; } String _createHourCondition(String column, String operator, dynamic value) { if (value == null) { throw InvalidArgumentException( 'The value for whereHour must not be null.', ); } final paramName = _nextParamName(); bindings[paramName] = value; return "HOUR($column) $operator :$paramName"; } String _createInCondition(String column, dynamic values, bool not) { String clause = not ? "NOT IN" : "IN"; if (values is List) { List paramNames = []; for (var i = 0; i < values.length; i++) { final paramName = _nextParamName(); bindings[paramName] = values[i]; paramNames.add(":$paramName"); } return "$column $clause (${paramNames.join(", ")})"; } else if (values is QueryBuilder) { final subQuery = values.toSql(); bindings.addAll((values as dynamic).getBindings()); return "$column $clause ($subQuery)"; } else { throw InvalidArgumentException( "The value for 'values' must be of type List or QueryBuilder.", ); } } String _createJsonContainsCondition(String column, dynamic value, bool not) { final paramName = _nextParamName(); bindings[paramName] = value; String condition = "JSON_CONTAINS($column, :$paramName)"; if (not) { condition = "NOT $condition"; } return condition; } String _createJsonLengthCondition( String column, String operator, dynamic value, ) { final paramName = _nextParamName(); bindings[paramName] = value; return "JSON_LENGTH($column) $operator :$paramName"; } String _createLikeCondition( String column, dynamic value, { bool not = false, bool caseSensitive = false, }) { final paramName = _nextParamName(); bindings[paramName] = value; String operator = not ? "NOT LIKE" : "LIKE"; if (!caseSensitive) { return "LOWER($column) $operator LOWER(:$paramName)"; } return "$column $operator :$paramName"; } String _createNullCondition(String column, bool not) { return "$column IS ${not ? 'NOT ' : ''}NULL"; } String _createRowValuesCondition( List columns, String operator, List values, ) { if (columns.length != values.length) { throw InvalidArgumentException( "The number of columns and values must be equal.", ); } List paramNames = []; for (var i = 0; i < values.length; i++) { final paramName = _nextParamName(); bindings[paramName] = values[i]; paramNames.add(":$paramName"); } String cols = "(${columns.join(", ")})"; String vals = "(${paramNames.join(", ")})"; return "$cols $operator $vals"; } String _processRawSQL(String sql, List rawBindings) { String processed = sql; for (var binding in rawBindings) { final paramName = _nextParamName(); bindings[paramName] = binding; processed = processed.replaceFirst('?', ':$paramName'); } return processed; } } ================================================ FILE: lib/src/database/query_builder/_window_functions_builder_impl.dart ================================================ import 'package:meta/meta.dart'; import '../../contract/database/query_builder/query_builder.dart' show QueryBuilder; import '../../exception/invalid_argument_exception.dart'; abstract mixin class WindowFunctionsBuilderImpl implements QueryBuilder { @protected final List _windowFunctions = []; String _buildOverClause({String? partitionBy, String? orderBy}) { List overParts = []; if (partitionBy != null && partitionBy.isNotEmpty) { overParts.add("PARTITION BY $partitionBy"); } if (orderBy != null && orderBy.isNotEmpty) { overParts.add("ORDER BY $orderBy"); } return "OVER (${overParts.join(' ')})"; } void _addWindowFunction( String functionName, String overClause, String? alias, ) { String windowFunc = "$functionName $overClause"; if (alias != null && alias.isNotEmpty) { windowFunc += " AS $alias"; } selectColumns.add(windowFunc); _windowFunctions.add(windowFunc); } @override QueryBuilder rowNumber({String? partitionBy, String? orderBy, String? as}) { final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("ROW_NUMBER()", overClause, as); return this; } @override QueryBuilder rank({String? partitionBy, String? orderBy, String? as}) { final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("RANK()", overClause, as); return this; } @override QueryBuilder denseRank({String? partitionBy, String? orderBy, String? as}) { final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("DENSE_RANK()", overClause, as); return this; } @override QueryBuilder lag( String column, { int offset = 1, dynamic defaultValue, String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for LAG function', ); } String lagFunc = "LAG($column, $offset"; if (defaultValue != null) { if (defaultValue is String) { lagFunc += ", '$defaultValue'"; } else { lagFunc += ", $defaultValue"; } } lagFunc += ")"; final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction(lagFunc, overClause, as); return this; } @override QueryBuilder lead( String column, { int offset = 1, dynamic defaultValue, String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for LEAD function', ); } String leadFunc = "LEAD($column, $offset"; if (defaultValue != null) { if (defaultValue is String) { leadFunc += ", '$defaultValue'"; } else { leadFunc += ", $defaultValue"; } } leadFunc += ")"; final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction(leadFunc, overClause, as); return this; } @override QueryBuilder firstValue( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for FIRST_VALUE function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("FIRST_VALUE($column)", overClause, as); return this; } @override QueryBuilder lastValue( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for LAST_VALUE function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("LAST_VALUE($column)", overClause, as); return this; } @override QueryBuilder ntile( int buckets, { String? partitionBy, String? orderBy, String? as, }) { if (buckets <= 0) { throw InvalidArgumentException( 'Number of buckets must be greater than 0', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("NTILE($buckets)", overClause, as); return this; } @override QueryBuilder percentRank({String? partitionBy, String? orderBy, String? as}) { final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("PERCENT_RANK()", overClause, as); return this; } @override QueryBuilder cumeDist({String? partitionBy, String? orderBy, String? as}) { final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("CUME_DIST()", overClause, as); return this; } @override QueryBuilder windowSum( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for SUM window function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("SUM($column)", overClause, as); return this; } @override QueryBuilder windowAvg( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for AVG window function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("AVG($column)", overClause, as); return this; } @override QueryBuilder windowCount( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for COUNT window function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("COUNT($column)", overClause, as); return this; } @override QueryBuilder windowMax( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for MAX window function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("MAX($column)", overClause, as); return this; } @override QueryBuilder windowMin( String column, { String? partitionBy, String? orderBy, String? as, }) { if (column.isEmpty) { throw InvalidArgumentException( 'Column name cannot be empty for MIN window function', ); } final overClause = _buildOverClause( partitionBy: partitionBy, orderBy: orderBy, ); _addWindowFunction("MIN($column)", overClause, as); return this; } @protected void clearWindowFunctions() { _windowFunctions.clear(); } } ================================================ FILE: lib/src/database/seeder/seeder.dart ================================================ import 'package:meta/meta.dart'; abstract class Seeder { @mustBeOverridden Future run(); } ================================================ FILE: lib/src/database/seeder/seeder_factory.dart ================================================ import 'dart:io'; import 'dart:math'; import 'package:meta/meta.dart'; abstract class SeederFactory { final Random _random = Random(); @mustBeOverridden Map definition(); Map make([Map? attributes]) { final data = definition(); if (attributes != null) { data.addAll(data); } return data; } List> makeMany( int count, [ Map? attributes, ]) { return List.generate(count, (index) => make(attributes)); } Map create([Map? attributes]) { return make(attributes); } List> createMany( int count, [ Map? attributes, ]) { return makeMany(count, attributes); } int randomInt(int min, int max) { return min + _random.nextInt(max - min + 1); } double randomDouble(double min, double max) { return min + _random.nextDouble() * (max - min); } bool randomBool() { return _random.nextBool(); } T randomElement(List list) { if (list.isEmpty) { stderr.write('List cannot be empty'); exit(0); } return list[_random.nextInt(list.length)]; } String randomString( int length, { bool includeNumbers = true, bool includeSymbols = false, }) { const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; const numbers = '0123456789'; const symbols = '!@#\$%^&*()_+-=[]{}|;:,.<>?'; String chars = letters; if (includeNumbers) chars += numbers; if (includeSymbols) chars += symbols; return String.fromCharCodes( Iterable.generate( length, (_) => chars.codeUnitAt(_random.nextInt(chars.length)), ), ); } String randomEmail() { final domains = [ 'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'protonmail.com', 'mail.com', 'yandex.com', 'zoho.com', 'gmx.com', 'live.com', 'msn.com', 'fastmail.com', 'tutanota.com', ]; final username = randomString( 8, includeNumbers: true, includeSymbols: false, ).toLowerCase(); final domain = randomElement(domains); return '$username@$domain'; } String randomName() { final firstNames = [ 'John', 'Jane', 'Michael', 'Sarah', 'David', 'Emily', 'Robert', 'Jessica', 'William', 'Ashley', 'James', 'Amanda', 'Christopher', 'Stephanie', 'Daniel', 'Melissa', 'Matthew', 'Nicole', 'Anthony', 'Elizabeth', 'Dorothy', 'Jerry', 'Helen', 'Tyler', 'Sandra', 'Aaron', 'Donna', 'Jose', 'Carol', 'Henry', 'Ruth', 'Douglas', 'Sharon', 'Zachary', 'Michelle', 'Nathan', 'Laura', 'Peter', 'Sarah', 'Kyle', 'Kimberly', 'Noah', 'Deborah', 'Jeremy', 'Dorothy', 'Carl', 'Lisa', 'Arthur', 'Nancy', 'Lawrence', 'Karen', 'Sean', 'Betty', 'Christian', 'Helen', 'Austin', 'Sandra', 'Wayne', 'Donna', 'Louis', 'Carol', 'Philip', 'Ruth', 'Eugene', 'Sharon', 'Ralph', 'Michelle', 'Roy', 'Laura', 'Billy', 'Emily', 'Bruce', 'Kimberly', 'Willie', 'Deborah', 'Jordan', 'Amy', 'Mason', 'Angela', 'Ethan', 'Brenda', 'Liam', 'Emma', 'Noah', 'Olivia', 'Oliver', 'Ava', 'Elijah', 'Sophia', 'Lucas', 'Isabella', 'Logan', 'Mia', 'Owen', 'Charlotte', 'Aiden', 'Amelia', 'Carter', 'Harper', 'Sebastian', 'Evelyn', ]; final lastNames = [ 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin', 'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark', 'Ramirez', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott', 'Torres', 'Nguyen', 'Hill', 'Flores', 'Green', 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera', 'Campbell', 'Mitchell', 'Carter', 'Roberts', 'Gomez', 'Phillips', 'Evans', 'Turner', 'Diaz', 'Parker', 'Cruz', 'Edwards', 'Collins', 'Reyes', 'Stewart', 'Morris', 'Morales', 'Murphy', 'Cook', 'Rogers', 'Gutierrez', 'Ortiz', 'Morgan', 'Cooper', 'Peterson', 'Bailey', 'Reed', 'Kelly', 'Howard', 'Ramos', 'Kim', 'Cox', 'Ward', 'Richardson', 'Watson', 'Brooks', 'Chavez', 'Wood', 'James', 'Bennett', 'Gray', 'Mendoza', 'Ruiz', 'Hughes', 'Price', 'Alvarez', 'Castillo', 'Sanders', 'Patel', 'Myers', 'Long', 'Ross', 'Foster', 'Jimenez', 'Powell', 'Jenkins', 'Perry', 'Russell', 'Sullivan', 'Bell', 'Coleman', 'Butler', 'Henderson', 'Barnes', 'Gonzales', 'Fisher', 'Vasquez', 'Simmons', 'Romero', 'Jordan', 'Patterson', 'Alexander', 'Hamilton', 'Graham', 'Reynolds', ]; return '${randomElement(firstNames)} ${randomElement(lastNames)}'; } String randomPhone() { return '+1${randomInt(100, 999)}${randomInt(100, 999)}${randomInt(1000, 9999)}'; } DateTime randomDate(DateTime start, DateTime end) { final diff = end.difference(start).inDays; final randomDays = _random.nextInt(diff + 1); return start.add(Duration(days: randomDays)); } DateTime randomPastDate([int maxDaysAgo = 365]) { final now = DateTime.now(); final daysAgo = _random.nextInt(maxDaysAgo + 1); return now.subtract(Duration(days: daysAgo)); } DateTime randomFutureDate([int maxDaysFromNow = 365]) { final now = DateTime.now(); final daysFromNow = _random.nextInt(maxDaysFromNow + 1); return now.add(Duration(days: daysFromNow)); } String randomUuid() { return '${randomString(8)}-${randomString(4)}-${randomString(4)}-${randomString(4)}-${randomString(12)}'; } String randomText([int sentences = 3]) { final words = [ 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor', 'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua', 'enim', 'ad', 'minim', 'veniam', 'quis', 'nostrud', 'exercitation', 'ullamco', 'laboris', 'nisi', 'aliquip', 'ex', 'ea', 'commodo', 'consequat', 'duis', 'aute', 'irure', 'in', 'reprehenderit', 'voluptate', 'velit', 'esse', 'cillum', 'fugiat', 'nulla', 'pariatur', 'excepteur', 'sint', 'occaecat', 'cupidatat', 'non', 'proident', 'sunt', 'culpa', 'qui', 'officia', 'deserunt', 'mollit', 'anim', 'id', 'est', 'laborum', ]; final result = []; for (int i = 0; i < sentences; i++) { final sentenceLength = randomInt(5, 15); final sentence = List.generate( sentenceLength, (_) => randomElement(words), ); sentence[0] = sentence[0][0].toUpperCase() + sentence[0].substring(1); result.add('${sentence.join(' ')}.'); } return result.join(' '); } double randomPrice([ double min = 1.0, double max = 1000.0, int decimals = 2, ]) { return double.parse(randomDouble(min, max).toStringAsFixed(decimals)); } String randomStatus([List? statuses]) { statuses ??= ['active', 'inactive', 'pending', 'completed']; return randomElement(statuses); } } ================================================ FILE: lib/src/database/seeder/seeder_runner.dart ================================================ import 'dart:io'; import '../../env_handler/env.dart'; import '../../exception/query_exception.dart'; import '../../utils/functions.dart'; import '../_connection_manager.dart'; import '../_database_utils/_db_config.dart'; import 'seeder.dart'; class SeederRunner { static final SeederRunner _singleton = SeederRunner._internal(); factory SeederRunner() { Env().load(); return _singleton; } SeederRunner._internal(); DBConfig _config(Map database) => DBConfig( driver: database['driver'] ?? '', host: database['host'] ?? '', port: database['port'] ?? '', database: database['database'] ?? '', username: database['username'] ?? '', password: database['password'] ?? '', sslMode: database['sslmode'] ?? '', collation: database['collation'] ?? '', pool: database['pool'] ?? false, poolSize: database['poolsize'] ?? 0, filePath: database['file_path'] ?? '', openInMemorySQLite: database['openInMemorySQLite'] ?? false, ); Future setup({ required Map database, required List seeders, }) async { if (database['default'] == null) { stderr.write('❌ Database config not valid'); exit(1); } try { ConnectionManager().defaultConnection = database['default']; Map connections = database['connections']; await ConnectionManager().connect( _config(connections[ConnectionManager().defaultConnection]), database['default'], ); } catch (e) { stderr.write(e); exit(1); } finally { for (Seeder seeder in seeders) { final stopwatch = Stopwatch()..start(); try { await seeder.run(); stopwatch.stop(); stderr.writeln( ' Seeder ${toSnakeCase(seeder.runtimeType.toString())} executed ....................................\x1B[32m ${stopwatch.elapsedMilliseconds}ms DONE\x1B[0m', ); } on QueryException catch (e) { stopwatch.stop(); stderr.write(e.cause); await ConnectionManager().connection(database['default'])!.close(); exit(1); } } await ConnectionManager().connection(database['default'])!.close(); stderr.write( '\x1B[32m All database seeders executed successfully \x1B[0m', ); exit(0); } } } ================================================ FILE: lib/src/enum/column_index.dart ================================================ enum ColumnIndex { unique, indexKey, fulltext, spatial } ================================================ FILE: lib/src/enum/http_request_method.dart ================================================ enum HttpRequestMethod { get, post, put, patch, delete, purge, options, copy, view, link, unlink, lock, unlock, propfind, } ================================================ FILE: lib/src/env_handler/env.dart ================================================ import 'dart:io'; class Env { static final Env _singleton = Env._internal(); factory Env() { return _singleton; } Map env = {}; Env._internal(); void load({File? file}) { if (env.isEmpty) { env = _loadEnvFile(file: file); } } /// get env value /// ``` /// Evn.get('APP_KEY'); /// Evn.get('APP_KEY', 'Default Value'); /// Evn.get('PORT', 3000); /// Evn.get('PORT', 3000); /// Evn.get('APP_KEY'); /// ``` static T get(String key, [dynamic defaultValue]) { dynamic value = Env().env[key]; value ??= Platform.environment[key]; value ??= defaultValue; if (T.toString() == 'int') { return int.parse(value.toString()) as T; } if (T.toString() == 'double') { return double.parse(value.toString()) as T; } if (T.toString() == 'num') { return num.parse(value.toString()) as T; } if (T.toString() == 'bool') { return bool.parse(value.toString()) as T; } return value; } /// load env from .env of project directory Map _loadEnvFile({File? file}) { Map data = {}; File envFile = file ?? File('.env'); if (!envFile.existsSync()) return data; String contents = envFile.readAsStringSync(); // splitting with new line for each variables List list = contents.split('\n'); for (String d in list) { // splitting with equal sign to get key and value List keyValue = d.toString().split('='); if (keyValue.first.isNotEmpty) { data[keyValue.first.trim()] = _getValue(keyValue); } } return data; } String _getValue(List elements) { if (elements.length > 1) { List elementsExceptFirst = elements.sublist(1); String value = elementsExceptFirst.join('='); return value .replaceAll('"', '') .replaceAll("'", '') .replaceAll('`', '') .trim(); } return ''; } } ================================================ FILE: lib/src/exception/base_http_exception.dart ================================================ import 'package:vania/src/http/response/response.dart'; class BaseHttpResponseException { final dynamic message; final ResponseType responseType; final int code; const BaseHttpResponseException({ required this.message, required this.code, this.responseType = ResponseType.json, }); Response response(bool isHtml) => isHtml ? Response.html(message) : Response.json(message is Map ? message : {'message': message}, code); } ================================================ FILE: lib/src/exception/database_exception.dart ================================================ class DatabaseException implements Exception { final String message; final dynamic cause; DatabaseException(this.message, [this.cause]); } ================================================ FILE: lib/src/exception/exception_handler.dart ================================================ import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/http/response/response.dart'; abstract class ExceptionHandler { const ExceptionHandler(); Response handle(T exception, Request? request); } abstract class GeneralExceptionHandler { const GeneralExceptionHandler(); Response? handle(dynamic exception, Request? request); } ================================================ FILE: lib/src/exception/forbidden_exception.dart ================================================ import 'dart:io'; import 'package:vania/src/exception/base_http_exception.dart'; import 'package:vania/src/http/response/response.dart'; class ForbiddenException extends BaseHttpResponseException { ForbiddenException({ super.message = 'Forbidden', super.code = HttpStatus.forbidden, super.responseType = ResponseType.json, }); } ================================================ FILE: lib/src/exception/http_exception.dart ================================================ import 'dart:io'; import 'base_http_exception.dart'; class HttpResponseException extends BaseHttpResponseException { HttpResponseException({super.message, super.code = HttpStatus.found}); } ================================================ FILE: lib/src/exception/internal_server_error.dart ================================================ import 'dart:io'; import 'base_http_exception.dart'; class InternalServerError extends BaseHttpResponseException { InternalServerError({ required super.message, super.code = HttpStatus.internalServerError, }); } ================================================ FILE: lib/src/exception/invalid_argument_exception.dart ================================================ class InvalidArgumentException { final String message; const InvalidArgumentException(this.message); } ================================================ FILE: lib/src/exception/not_found_exception.dart ================================================ import 'dart:io'; import '../http/response/response.dart'; import 'base_http_exception.dart'; class NotFoundException extends BaseHttpResponseException { NotFoundException({ super.message = 'Not Fount 404', super.code = HttpStatus.notFound, super.responseType = ResponseType.html, }); } ================================================ FILE: lib/src/exception/page_expired_exception.dart ================================================ import 'package:vania/src/http/response/response.dart'; import 'base_http_exception.dart'; class PageExpiredException extends BaseHttpResponseException { const PageExpiredException({ super.message = '

Page Expired (419)

', super.code = 419, super.responseType = ResponseType.html, }); } ================================================ FILE: lib/src/exception/query_exception.dart ================================================ class QueryException implements Exception { final String? cause; QueryException([this.cause]); } ================================================ FILE: lib/src/exception/redirect_exception.dart ================================================ import 'dart:io'; import '../http/response/response.dart'; import 'base_http_exception.dart'; class RedirectException extends BaseHttpResponseException { RedirectException({ super.message, super.code = HttpStatus.found, super.responseType = ResponseType.html, }); } ================================================ FILE: lib/src/exception/throttle_exception.dart ================================================ import '../http/response/response.dart'; import 'base_http_exception.dart'; class ThrottleException extends BaseHttpResponseException { final Map? headers; ThrottleException({ required String super.message, required super.code, this.headers, }); @override Response response(bool isHtml) { if (isHtml) { return Response.html(message); } final Map responseData = message is Map ? Map.from(message as Map) : {'message': message}; if (headers != null) { return Response.jsonWithHeader( responseData, statusCode: code, headers: headers!, ); } return Response.json(responseData, code); } } ================================================ FILE: lib/src/exception/unauthenticated.dart ================================================ import 'dart:io'; import 'base_http_exception.dart'; class Unauthenticated extends BaseHttpResponseException { Unauthenticated({ required super.message, super.code = HttpStatus.unauthorized, super.responseType, }); } ================================================ FILE: lib/src/exception/unauthorized_exception.dart ================================================ import 'base_http_exception.dart'; class UnauthorizedException extends BaseHttpResponseException { UnauthorizedException({required super.message, required super.code}); } ================================================ FILE: lib/src/exception/validation_exception.dart ================================================ import 'dart:io'; import 'base_http_exception.dart'; class ValidationException extends BaseHttpResponseException { ValidationException({ required super.message, super.code = HttpStatus.unprocessableEntity, }); } ================================================ FILE: lib/src/extensions/date_time_extension.dart ================================================ extension DateTimeExtension on DateTime { /// Converts a DateTime object to a string formatted according to AWS datetime standards. /// /// This method formats the DateTime object into a compact string suitable for AWS services, /// such as those requiring a timestamp in ISO 8601 format but without hyphens, colons, /// or spaces. It ends with a 'Z' to indicate Zulu time (UTC). /// /// **Returns:** /// A string representing the DateTime in AWS format: `YYYYMMDDTHHMMSSZ` /// /// **Example Usage:** /// ```dart /// var now = DateTime.now(); /// var awsFormatted = now.toAwsFormat(); /// print(awsFormatted); // Outputs: '20230803T142530Z' (varies based on current time) /// ``` String toAwsFormat() { String zeroPad(int number) => number.toString().padLeft(2, '0'); return '${zeroPad(year)}${zeroPad(month)}${zeroPad(day)}T' '${zeroPad(hour)}${zeroPad(minute)}${zeroPad(second)}Z'; } /// Formats a DateTime object into a human-readable string. /// /// This method converts the DateTime object into a standard datetime string format /// commonly used in various systems for displaying date and time. /// /// **Returns:** /// A string in the format `YYYY-MM-DD HH:MM:SS`, which is more readable than the compact AWS format. /// /// **Example Usage:** /// ```dart /// var now = DateTime.now(); /// var formatted = now.format(); /// print(formatted); // Outputs: '2023-08-03 14:25:30' (varies based on current time) /// ``` String format() { return '${year.toString().padLeft(4, '0')}-' '${month.toString().padLeft(2, '0')}-' '${day.toString().padLeft(2, '0')} ' '${hour.toString().padLeft(2, '0')}:' '${minute.toString().padLeft(2, '0')}:' '${second.toString().padLeft(2, '0')}'; } } ================================================ FILE: lib/src/extensions/extensions.dart ================================================ export 'date_time_extension.dart'; export 'map_extension.dart'; export 'number_extension.dart'; export 'string_extension.dart'; export 'string_list_extension.dart'; export 'localization_extension.dart'; ================================================ FILE: lib/src/extensions/localization_extension.dart ================================================ import 'package:vania/src/localization_handler/localization.dart'; extension LocalizationExtension on String { String trans({Map? args, String? locale}) { return Localization().trans(this, args, locale); } } ================================================ FILE: lib/src/extensions/map_extension.dart ================================================ extension MapExtensions on Map { /// 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. /// /// **Parameters:** /// - [keys]: A string representing the path to the value to remove, separated by dots. For example, 'user.profile.name'. /// /// **Returns:** /// The original map with the specified parameter removed if found. If any part of the path is invalid (not a map or non-existent key), the original map is returned unchanged. /// /// **Example Usage:** /// ```dart /// var myMap = { /// 'user': { /// 'profile': { /// 'name': 'John', /// 'age': 30 /// } /// } /// }; /// myMap.removeParam('user.profile.name'); /// print(myMap); // Outputs: {user: {profile: {age: 30}}} /// myMap.removeParam('user.profile.nonExistentKey'); // Does nothing if key does not exist /// print(myMap); // Outputs: {user: {profile: {age: 30}}} /// ``` Map removeParam(String keys) { List parts = keys.split('.'); Map data = this; for (int i = 0; i < parts.length - 1; i++) { if (data[parts[i]] is Map) { data = data[parts[i]]; } else { return this; // Early exit if path is invalid } } data.remove(parts.last); return this; } /// Retrieves a parameter from a nested map structure based on a dot-separated key path. /// /// This method facilitates accessing values in a deeply nested map using a path described by a string of keys separated by dots. /// /// **Parameters:** /// - [keys]: A string representing the path to the value, separated by dots. For example, 'user.profile.age'. /// /// **Returns:** /// The value at the specified path if it exists and the path is valid. If the path is broken at any point (not a map or key does not exist), returns `null`. /// /// **Example Usage:** /// ```dart /// var myMap = { /// 'user': { /// 'profile': { /// 'name': 'John', /// 'age': 30 /// } /// } /// }; /// var name = myMap.getParam('user.profile.name'); /// print(name); // Outputs: John /// var salary = myMap.getParam('user.profile.salary'); // Path does not exist /// print(salary); // Outputs: null /// ``` dynamic getParam(String keys) { List parts = keys.split('.'); Map data = this; for (int i = 0; i < parts.length - 1; i++) { if (data[parts[i]] is Map) { data = data[parts[i]]; } else { return []; } } if (data[parts.last] is List) { List> list = List.castFrom>(data[parts.last]); return list; } return data[parts.last]; } } ================================================ FILE: lib/src/extensions/number_extension.dart ================================================ extension NumberExtension on num { /// Rounds the number to a fixed number of decimal places. /// /// This method rounds the calling number to the specified number of decimal places /// and returns it as a `num`. If the number is an integer, it will return the same integer /// if `decimal` is zero; otherwise, it will return a decimal with the specified number of places. /// /// **Parameters:** /// - [decimal]: The number of decimal places to round the number to. This must be a non-negative integer. /// /// **Returns:** /// A new number rounded to the specified number of decimal places. /// /// **Example Usage:** /// ```dart /// var number = 123.4567; /// var rounded = number.toFixed(2); /// print(rounded); // Outputs: 123.46 /// /// var integer = 123; /// var roundedInt = integer.toFixed(0); /// print(roundedInt); // Outputs: 123 /// /// var negative = -123.456; /// var roundedNeg = negative.toFixed(3); /// print(roundedNeg); // Outputs: -123.456 /// ``` /// /// **Note:** /// The method uses `toStringAsFixed` and `num.parse` to perform the rounding, which means /// the method handles rounding similarly to how JavaScript handles number rounding. num toFixed(int decimal) { return num.parse(toStringAsFixed(decimal)); } } ================================================ FILE: lib/src/extensions/string_extension.dart ================================================ extension StringExtension on String { /// Converts a string to an integer. /// /// Attempts to parse the string as an integer using `int.tryParse`. If the string /// cannot be parsed into an integer (e.g., because it contains letters or special characters), /// this method returns `null`. /// /// **Examples:** /// - For a valid integer string: /// ```dart /// var number = '123'.toInt(); // 123 /// ``` /// - For a string containing non-digit characters: /// ```dart /// var number = '123abc'.toInt(); // null /// ``` /// - For a string representing a floating-point number: /// ```dart /// var number = '123.45'.toInt(); // null /// ``` /// - For an empty string: /// ```dart /// var number = ''.toInt(); // null /// ``` /// /// **Parameters:** /// None. /// /// **Returns:** int? toInt() { return int.tryParse(this); } } ================================================ FILE: lib/src/extensions/string_list_extension.dart ================================================ extension StringListExtension on List { /// Joins a list of strings into a single string with a custom separator and a final conjunction word. /// /// This method concatenates each element of the list into a string, separated by [separator], /// and uses [lastJoinText] before the final element to format the output naturally in English. /// /// **Examples:** /// - For an empty list: returns an empty string. /// - For a list with two elements: returns the two elements separated by [lastJoinText]. /// - For a list with more than two elements: returns all elements separated by [separator] and /// the last two elements are joined with [lastJoinText]. /// /// **Code Examples:** /// ```dart /// var value = [].joinWithAnd(); // '' /// value = ['apple'].joinWithAnd(); // 'apple' /// value = ['apple', 'orange'].joinWithAnd(); // 'apple and orange' /// value = ['apple', 'orange', 'banana'].joinWithAnd(); // 'apple, orange and banana' /// ``` /// Parameters: /// - [separator] (optional): the string to use between each element except before the last element. Defaults to ', '. /// - [lastJoinText] (optional): the string to use before the last element. Defaults to 'and'. /// /// Returns: /// The concatenated string of the list elements formatted according to the rules described. String joinWithAnd([String separator = ', ', String lastJoinText = 'and']) { if (isEmpty) return ''; if (length == 1) return first; if (length == 2) return '$first $lastJoinText $last'; return '${sublist(0, length - 1).join(separator)} $lastJoinText $last'; } } ================================================ FILE: lib/src/http/controller/controller.dart ================================================ abstract class Controller { const Controller(); } ================================================ FILE: lib/src/http/controller/controller_handler.dart ================================================ import 'package:vania/src/exception/database_exception.dart'; import 'package:vania/src/exception/exception_handler.dart'; import 'package:vania/src/exception/query_exception.dart'; import 'package:vania/src/exception/validation_exception.dart'; import 'package:vania/src/http/request/request.dart'; 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 'package:vania/application.dart'; import '../../exception/base_http_exception.dart'; import '../../exception/invalid_argument_exception.dart'; class ControllerHandler { dynamic _getParamValue(String param) { if (int.tryParse(param) != null) { return int.parse(param); } else if (num.tryParse(param) != null) { return double.parse(param); } else if (double.tryParse(param) != null) { return num.parse(param); } else if (param.toLowerCase() == 'true') { return true; } else if (param.toLowerCase() == 'false') { return false; } else { return param; } } void create({required RouteData route, required Request request}) async { List positionalArguments = []; if (route.params != null) { try { positionalArguments = route.params!.values .map((param) => _getParamValue(param.toString())) .toList(); } on FormatException catch (e) { _response(request, e.message, 500); } } if (route.hasRequest) { positionalArguments.insert(0, request); } try { Response response = await Function.apply( route.action, positionalArguments, {}, ); response.makeResponse(request.response); } on ValidationException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } bool isHtml = request.request.headers .value('accept') .toString() .contains('html'); if (isHtml) { Response.redirect( RouteHistory().previousRoute, ).makeResponse(request.response); } else { error.response(false).makeResponse(request.response); } } on InvalidArgumentException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } _response(request, error.message); } on DatabaseException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } _response(request, error.message, 500); } on QueryException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } _response(request, error.cause ?? '', 500); } on BaseHttpResponseException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } _response(request, error.message, error.code); } catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(request.response); } _response(request, error.toString()); } } Response? _handleException(dynamic exception, Request request) { try { ExceptionHandler? handler = Application().getExceptionHandler(exception.runtimeType); if (handler != null) { return handler.handle(exception, request); } GeneralExceptionHandler? generalHandler = Application().getGeneralExceptionHandler(); if (generalHandler != null) { return generalHandler.handle(exception, request); } } catch (_) {} return null; } } void _response(Request req, message, [statusCode = 500]) { if (req.headers['accept'].toString().contains('html')) { Response.html(message).makeResponse(req.response); } else { Response.json({"message": message}, statusCode).makeResponse(req.response); } } ================================================ FILE: lib/src/http/middleware/middleware.dart ================================================ import 'dart:io'; import 'package:vania/src/http/request/request.dart'; abstract class Middleware { Future handle(Request req); } abstract class WebSocketMiddleware { Future handle(HttpRequest req); } ================================================ FILE: lib/src/http/middleware/middleware_handler.dart ================================================ import 'package:vania/src/http/request/request.dart'; import 'middleware.dart'; Future middlewareHandler( List middlewares, Request request, ) async { for (Middleware middleware in middlewares) { await middleware.handle(request); } } ================================================ FILE: lib/src/http/middleware/web_socket_middleware_handler.dart ================================================ import 'dart:io'; import 'middleware.dart'; Future webSocketMiddlewareHandler( List middlewares, HttpRequest request, ) async { for (WebSocketMiddleware middleware in middlewares) { await middleware.handle(request); } } ================================================ FILE: lib/src/http/request/request.dart ================================================ 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'; import 'package:vania/src/http/validation/custom_validation_rule.dart'; import 'package:vania/src/http/validation/validation_chain/validation.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart' show ValidationRule; 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 { late HttpRequest request; RouteData? route; Request from({required HttpRequest request, RouteData? route}) { this.request = request; this.route = route; return this; } Map? get user => Auth().user(); String? get ip => request.connectionInfo?.remoteAddress.address; HttpHeaders get _httpHeaders => request.headers; ContentType? get contentType => request.headers.contentType; Uri get uri => request.uri; String? get path => route?.path; String get url => "$host$uri"; String get host => header(HttpHeaders.hostHeader) ?? 'unknown'; String? get method => route?.method; HttpResponse get response => request.response; Map get _all => all(); Map get _query => uri.queryParameters; Map body = {}; final Map _cookies = {}; List? _customRules; Request setCustomRule(List customRule) { _customRules = customRule; return this; } /// Gets a cookie by name and casts it to type [T]. /// /// If the cookie doesn't exist, it returns `null`. /// /// If [T] is [String], it's casted to a string. /// If [T] is [bool], it's parsed from a string. /// If [T] is [int], it's parsed from a string. /// If [T] is [double], it's parsed from a string. /// Otherwise, it's casted to [T]. /// T? cookie(String key) { if (_cookies[key] == null) return null; return switch (T.toString()) { 'String' => _cookies[key].toString(), 'bool' => bool.parse(_cookies[key]) as T, 'int' => int.parse(_cookies[key]) as T, 'double' => double.parse(_cookies[key]) as T, (_) => _cookies[key], }; } /// Extracts the cookies from the headers and stores them in [_cookies]. /// /// The format of the [HttpHeaders.cookieHeader] is: void _extractCookies() { List? cookies = _httpHeaders[HttpHeaders.cookieHeader]; if (cookies == null) { return; } for (String cookie in cookies) { List cookies = cookie.split(';'); for (String cookie in cookies) { List cookieList = cookie.split('='); _cookies[cookieList[0].trim()] = cookieList[1].trim(); } } } Future extractBody() async { _extractCookies(); final whereMethod = [ 'post', 'patch', 'put', 'delete', ].where((method) => method == request.method.toLowerCase()).toList(); if (whereMethod.isNotEmpty) { body = await RequestBody.extractBody(request: request); } return this; } Map all() { return {...body, ..._query, ...params()}; } Map params() { final vParams = route?.params ?? {}; vParams.removeWhere((key, value) => value is Request); return vParams; } bool isMethod(String method) { return route?.method.toLowerCase() == method.toLowerCase(); } Map only(List keys) { Map ret = {}; for (String key in keys) { ret[key] = _all[key]; } return ret; } bool has(dynamic keys) { if (keys is String) { String? val = _all[keys]; if (val == null) { return false; } return val.toString().isNotEmpty ? true : false; } if (keys is List) { bool hasKey = true; for (String key in keys) { if (_all[key] == null) { hasKey = false; } } return hasKey; } return (_all[keys] != null && _all[keys].toString().isNotEmpty); } bool hasAny(List keys) { bool hasKey = false; for (String key in keys) { if (_all[key] != null && _all[key].toString().isNotEmpty) { hasKey = true; } } return hasKey; } Future whenHas(String key) async { if (_all[key] != null) { return Future.value(_all[key]); } else { return Future.error(""); } } Map except(dynamic key) { Map requestItems = _all; if (key is List) { for (String vKey in key) { requestItems.removeWhere((iKey, value) => iKey == vKey); } } if (key is String) { requestItems.removeWhere((vkey, value) => vkey == key); } return requestItems; } Map json(String key) { if (_all[key] != null && _all[key] is String) { return jsonDecode(_all[key]); } if (_all[key] != null && _all[key] is Map) { return _all[key]; } return {}; } dynamic input([String? key, dynamic defaultVal]) { if (key == null) { return _all; } if (_all[key] != null) { return int.tryParse(_all[key].toString()) ?? _all[key]; } if (defaultVal != null) { return defaultVal; } return null; } RequestFile? file(String key) { if (_all[key] == null) { return null; } if (_all[key] is! RequestFile) { return (_all[key] as List).first; } return _all[key]; } bool hasFile(String key) => (_all[key].toString().isNotEmpty && (file(key) != null || files(key) != null)); List? files(String key) { if (_all[key] == null) { return null; } var files = _all[key]; if (files is! List) { return [files]; } return files as List; } String string(String key) { return _all[key].toString(); } List asList(String key) { return List.from(_all[key]); } int? integer(String key) { return int.tryParse(_all[key].toString()); } double? asDouble(String key) { return double.tryParse(_all[key].toString()); } bool boolean(String key) { try { return bool.parse(_all[key].toString()); } catch (_) { return false; } } DateTime? date(String key) { try { return DateTime.parse(_all[key].toString()); } catch (_) { return null; } } dynamic query([String? key, String? defaultVal]) { if (key == null) { return _query.values; } if (_query[key] != null) { return _query[key]; } if (defaultVal != null) { return defaultVal; } return null; } void merge(Map values) { _all.addAll(values); body = {...body, ...values}; } void mergeIfMissing(Map values) { for (var vKey in values.keys) { if (!_all.keys.contains(vKey)) { _all.addEntries(values[vKey]); } } } String? header(String key, [String? defaultHeader]) { return _httpHeaders.value(key) ?? defaultHeader; } Map get headers { Map ret = {}; _httpHeaders.forEach((String name, List values) { ret[name] = values.join(); }); return ret; } bool isFormData() { return RequestBody.isFormData(contentType); } bool isJson() { return RequestBody.isJson(contentType); } bool isUrlencoded() { return RequestBody.isUrlencoded(contentType); } String? userAgent() { return header(HttpHeaders.userAgentHeader); } String? origin() { return header('origin'); } String? referer() { return header(HttpHeaders.refererHeader); } Future validate( dynamic rules, [ Map messages = const {}, ]) async { assert( rules is Map || rules is List || rules is List || rules is FormValidation, 'Rules must be either Map or List. or FormRequest', ); TemplateEngine().sessionErrors.clear(); if (rules is Map) { await _validate(rules, messages); } else if (rules is List) { Map 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); } } Future _formRequestValidate(FormValidation formRequest) async { if (!formRequest.authorize()) { throw UnauthorizedException(message: 'Access denied', code: 403); } final rules = formRequest.rules(); final Map messages = formRequest.messages(); Validator validator = Validator(data: body); if (formRequest.customRule().isNotEmpty) { validator.customRule(formRequest.customRule()); } Map rulesMap = {}; if (rules is List) { 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 _validate( Map rules, [ Map messages = const {}, ]) async { Validator validator = Validator(data: body); if (_customRules != null) { validator.customRule(_customRules!); } if (messages.isNotEmpty) { validator.setNewMessages(messages); } await validator.validate(rules); if (validator.hasError) { bool isHtml = request.headers.value('accept').toString().contains('html'); if (isHtml) { TemplateEngine().sessionErrors.addAll(validator.errors); } throw ValidationException(message: validator.errors); } } void _validateChain(List validations) { Map errors = {}; final data = all(); for (Validation validation in validations) { dynamic fieldValue = data.containsKey(validation.field) ? data[validation.field] : null; for (ValidationRule rule in validation.rules) { if (!rule.validate(fieldValue, data)) { errors[validation.field] = rule.errorMessage; break; } } } if (errors.isNotEmpty) { bool isHtml = request.headers.value('accept').toString().contains('html'); if (isHtml) { TemplateEngine().sessionErrors.addAll(errors); } throw ValidationException(message: errors); } } Map toJson() { Map ret = {}; _all.forEach((String key, dynamic value) { if (value is RequestFile) { ret[key] = value.filename; } else { ret[key] = value; } }); return ret; } } ================================================ FILE: lib/src/http/request/request_body.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:vania/src/http/request/request_form_data.dart'; import 'package:vania/src/utils/helper.dart' show abort; import 'package:vania/vania.dart' show env; class RequestBody { const RequestBody(); static final int _maxBodySizeBytes = env( 'MAX_BODY_SIZE', 10 * 1024 * 1024, ); static Future> extractBody({ required HttpRequest request, }) async { final headers = request.headers; final contentLength = headers.contentLength; if (contentLength > _maxBodySizeBytes) { abort( 413, 'Request body too large: $contentLength bytes (max $_maxBodySizeBytes bytes)', ); } if (isFormData(request.headers.contentType)) { final formData = RequestFormData(request: request); await formData.extractData(); return formData.inputs; } final bytesBuilder = BytesBuilder(); await for (final chunk in request) { bytesBuilder.add(chunk); } final bodyBytes = bytesBuilder.takeBytes(); final bodyString = utf8.decode(bodyBytes); if (isJson(request.headers.contentType)) { try { final decoded = jsonDecode(bodyString); if (decoded is Map) { return decoded; } } catch (_) {} return {}; } if (isUrlencoded(request.headers.contentType)) { try { return Uri.splitQueryString(bodyString); } catch (_) { return {}; } } return {}; } static bool isUrlencoded(ContentType? contentType) { return contentType?.mimeType.toLowerCase().contains('urlencoded') == true; } static bool isFormData(ContentType? contentType) { return contentType?.mimeType.toLowerCase().contains('form-data') == true; } static bool isJson(ContentType? contentType) { return contentType?.mimeType.toLowerCase().contains('json') == true; } } ================================================ FILE: lib/src/http/request/request_file.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:mime/mime.dart'; import 'package:vania/src/storage/storage.dart'; import 'package:vania/src/utils/functions.dart'; /// Represents an uploaded file part from multipart/form-data. /// Provides lazy access to bytes, size, and easy storage/move. class RequestFile { final String filename; final String filetype; final MimeMultipart stream; Uint8List? _bytes; RequestFile({ required this.filename, required this.filetype, required this.stream, }); /// File extension without the dot (e.g. "png", "jpg", "pdf"). String get extension { final idx = filename.lastIndexOf('.'); return (idx >= 0 && idx < filename.length - 1) ? filename.substring(idx + 1).toLowerCase() : ''; } /// Lazily reads all bytes from the multipart stream. Future get bytes async { if (_bytes != null) return _bytes!; final builder = BytesBuilder(); await for (final chunk in stream) { builder.add(chunk); } _bytes = builder.takeBytes(); return _bytes!; } /// Returns the file size in bytes. Future get size async { final b = await bytes; return b.length; } /// Original client‐provided file name. String get clientOriginalName => filename; /// Original client‐provided file extension. String get clientOriginalExtension => extension; /// Original client‐provided MIME type. String get clientMimeType => filetype; /// Store the file via your Storage layer. /// - `destPath` should include trailing slash if desired. Future store({String path = '', required String name}) async { try { final content = await bytes; return await Storage.put(path, name, content.toList()); } catch (e) { throw FileSystemException('Failed to store file as $name: $e'); } } /// Move the file into a local path on disk. /// Creates directories as needed. Future move({required String toPath, required String name}) async { final fullPath = sanitizeRoutePath('$toPath/$name'); final file = File(fullPath); await file.parent.create(recursive: true); final sink = file.openWrite(); await for (final chunk in stream) { sink.add(chunk); } await sink.close(); // strip leading /public if used return fullPath.replaceFirst(RegExp(r'^/?public'), ''); } } ================================================ FILE: lib/src/http/request/request_form_data.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:mime/mime.dart'; import 'request_file.dart'; /// Handles extraction of multipart/form-data requests into a simple /// Map of field names to either String/int or RequestFile instances. class RequestFormData { final HttpRequest request; final Map inputs = {}; RequestFormData({required this.request}); /// Parse and collect all form-data parts from the request. /// - For text fields: decodes UTF-8 and converts to int if possible. /// - For file fields: wraps them in a RequestFile. Future extractData() async { final boundary = request.headers.contentType?.parameters['boundary']; if (boundary == null) { throw HttpException('Missing multipart boundary', uri: request.uri); } // split request stream into MimeMultipart parts final transformer = MimeMultipartTransformer(boundary); final parts = await request .cast>() .transform(transformer) .toList(); for (final part in parts) { final disposition = part.headers['content-disposition']; if (disposition == null) continue; // Simple regex to capture name="..." and filename="..." pairs final params = {}; final regExp = RegExp(r'([\w\-]+)="([^"]*)"'); for (final m in regExp.allMatches(disposition)) { params[m.group(1)!] = m.group(2)!; } final name = params['name']; if (name == null) continue; final filename = params['filename']; if (filename == null || filename.isEmpty) { // text field final raw = utf8.decode( await part.fold>([], (buf, b) => buf..addAll(b)), ); final value = int.tryParse(raw) ?? raw; if (name.endsWith('[]')) { final key = name.substring(0, name.length - 2); inputs.putIfAbsent(key, () => []); (inputs[key] as List).add(value); } else { inputs[name] = value; } } else { // file field final file = RequestFile( filename: filename, filetype: part.headers['content-type'] ?? 'application/octet-stream', stream: part, ); if (name.endsWith('[]')) { final key = name.substring(0, name.length - 2); inputs.putIfAbsent(key, () => []); (inputs[key] as List).add(file); } else { inputs[name] = file; } } } return this; } } ================================================ FILE: lib/src/http/request/request_handler.dart ================================================ import 'dart:io'; import 'package:vania/src/exception/database_exception.dart'; import 'package:vania/src/exception/exception_handler.dart'; import 'package:vania/src/exception/query_exception.dart'; import 'package:vania/src/extensions/extensions.dart'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/http/session/session_manager.dart'; import 'package:vania/src/route/middleware/csrf_middleware.dart'; import 'package:vania/src/view_engine/helper.dart'; import 'package:vania/src/config/http_cors.dart'; import 'package:vania/src/exception/internal_server_error.dart'; import 'package:vania/src/exception/invalid_argument_exception.dart'; import 'package:vania/src/exception/page_expired_exception.dart'; import 'package:vania/src/exception/not_found_exception.dart'; import 'package:vania/src/exception/unauthenticated.dart'; import 'package:vania/src/http/controller/controller_handler.dart'; import 'package:vania/src/http/middleware/middleware_handler.dart'; import 'package:vania/src/ioc_container.dart'; import 'package:vania/src/route/route_data.dart'; import 'package:vania/src/route/route_handler.dart'; import 'package:vania/src/route/route_history.dart'; import 'package:vania/src/view_engine/template_engine.dart'; import 'package:vania/src/websocket/web_socket_handler.dart'; import 'package:vania/src/exception/base_http_exception.dart'; import 'package:vania/src/logger/logger.dart'; import 'package:vania/src/utils/helper.dart'; import 'package:vania/application.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 /// to the WebSocketHandler; otherwise, it processes the request by checking /// CORS, handling routes, invoking middleware, and executing the appropriate /// controller action. The function also manages session initiation and logs /// request details in debug mode. /// /// Throws: /// - [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('APP_WEBSOCKET', false) && WebSocketTransformer.isUpgradeRequest(req)) { WebSocketHandler().handler(req); } else { bool isHtml = req.headers.value('accept').toString().contains('html'); Request? request; try { HttpCors(req); RouteData? route = httpRouteHandler(req); DateTime startTime = DateTime.now(); String requestUri = req.uri.path; String starteRequest = startTime.format(); String requestMethod = req.method.toUpperCase(); if (route != null) { request = Request().from(request: req, route: route); await request.extractBody(); if (isHtml) { TemplateEngine().formData.addAll(request.all()); await IoCContainer().resolve().sessionStart( req, req.response, ); RouteHistory().updateRouteHistory(req); } if (env('CSRF_PROTECTION_ENABLED', false)) { route.preMiddleware.add(CsrfMiddleware()); } /// Check if pre middleware exist and call it if (route.preMiddleware.isNotEmpty) { await middlewareHandler(route.preMiddleware, request); } /// Controller and method handler ControllerHandler().create(route: route, request: request); if (env('APP_DEBUG')) { var endTime = DateTime.now(); var duration = endTime.difference(startTime).inMilliseconds; var requestedPath = requestUri.isNotEmpty ? requestUri.padRight(118 - requestUri.length, '.') : ''.padRight(118, '.'); stderr.writeln( '$starteRequest $requestMethod $requestedPath ~ ${duration}ms', ); } } } on BaseHttpResponseException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(req.response); } if (error is NotFoundException && isHtml) { if (File('lib/resources/view/errors/404.html').existsSync()) { return view('errors/404').makeResponse(req.response); } } if (error is InternalServerError && isHtml) { if (File('lib/resources/view/errors/500.html').existsSync()) { return view('errors/500').makeResponse(req.response); } } if (error is PageExpiredException && isHtml) { if (File('lib/resources/view/errors/419.html').existsSync()) { return view('errors/419').makeResponse(req.response); } } if (error is Unauthenticated && isHtml) { return Response.redirect(error.message).makeResponse(req.response); } if (error is RedirectException && isHtml) { return Response.redirect(error.message).makeResponse(req.response); } error.response(isHtml).makeResponse(req.response); } on InvalidArgumentException catch (e) { Response? customResponse = _handleException(e, request); if (customResponse != null) { return customResponse.makeResponse(req.response); } Logger.log(e.message, type: Logger.ERROR); _response(req, e.message); } on DatabaseException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(req.response); } _response(req, error.message); } on QueryException catch (error) { Response? customResponse = _handleException(error, request); if (customResponse != null) { return customResponse.makeResponse(req.response); } _response(req, error.cause); } catch (e) { Response? customResponse = _handleException(e, request); if (customResponse != null) { return customResponse.makeResponse(req.response); } Logger.log(e.toString(), type: Logger.ERROR); _response(req, e.toString()); } } } Response? _handleException(dynamic exception, Request? request) { try { ExceptionHandler? handler = Application().getExceptionHandler(exception.runtimeType); if (handler != null) { return handler.handle(exception, request); } GeneralExceptionHandler? generalHandler = Application().getGeneralExceptionHandler(); if (generalHandler != null) { return generalHandler.handle(exception, request); } } catch (_) {} return null; } void _response(HttpRequest req, dynamic message, {int statusCode = 500}) { if (req.headers.value('accept').toString().contains('html')) { Response.html(message).makeResponse(req.response); } else { Response.json({ "message": message, }, statusCode).makeResponse(req.response); } } } ================================================ FILE: lib/src/http/response/response.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:vania/src/http/response/stream_file.dart'; import 'package:vania/src/route/route_history.dart'; import 'package:vania/src/view_engine/template_engine.dart'; enum ResponseType { json, none, redirect, html, sse, streamFile, download } class Response { @protected final ResponseType responseType; @protected final dynamic data; @protected final int httpStatusCode; @protected final Map headers; Response({ this.data, this.responseType = ResponseType.none, this.httpStatusCode = HttpStatus.ok, this.headers = const {}, }); @protected Future sseHandler(HttpResponse res) async { res.headers.contentType = ContentType.parse('text/event-stream'); res.headers.add(HttpHeaders.cacheControlHeader, 'no-cache'); res.headers.add(HttpHeaders.connectionHeader, 'keep-alive'); res.headers.add(HttpHeaders.transferEncodingHeader, 'chunked'); void writeSSE(String data) { res.add(utf8.encode('data: $data\n\n')); } await for (var event in data) { writeSSE(jsonEncode(event)); await res.flush(); } await res.close(); } void makeResponse(HttpResponse res) async { res.statusCode = httpStatusCode; if (headers.isNotEmpty) { headers.forEach((key, value) { res.headers.set(key, value); }); } switch (responseType) { case ResponseType.json: res.headers.contentType = ContentType.json; try { res.write(jsonEncode(data)); } catch (e) { res.write('jsonEncode Error: $e'); } await res.close(); break; case ResponseType.html: res.headers.contentType = ContentType.html; res.write(data); await res.close(); break; case ResponseType.sse: await sseHandler(res); break; case ResponseType.streamFile: StreamFile? stream = StreamFile( fileName: data['fileName'], bytes: data['bytes'], ).call(); if (stream == null) { res.headers.contentType = ContentType.json; res.write(jsonEncode({"message": "File not found"})); await res.close(); break; } res.headers.contentType = stream.contentType; res.headers.contentLength = stream.length; res.addStream(stream.stream!).then((_) => res.close()); break; case ResponseType.download: StreamFile? stream = StreamFile( fileName: data['fileName'], bytes: data['bytes'], ).call(); if (stream == null) { res.headers.contentType = ContentType.json; res.write(jsonEncode({"message": "File not found"})); await res.close(); break; } res.headers.contentType = stream.contentType; res.headers.contentLength = stream.length; res.headers.add("Content-Disposition", stream.contentDisposition); res.addStream(stream.stream!).then((_) => res.close()); break; case ResponseType.redirect: res.headers.set(HttpHeaders.locationHeader, data); await res.close(); default: res.write(data); await res.close(); } } static Response redirect(String location) => Response( responseType: ResponseType.redirect, data: location, httpStatusCode: HttpStatus.found, ); static Response json(dynamic jsonData, [int statusCode = HttpStatus.ok]) => Response( data: jsonData, responseType: ResponseType.json, httpStatusCode: statusCode, ); static Response jsonWithHeader( dynamic jsonData, { int statusCode = HttpStatus.ok, Map headers = const {}, }) => Response( data: jsonData, responseType: ResponseType.json, httpStatusCode: statusCode, headers: headers, ); static Response html( dynamic htmlData, { Map headers = const {}, }) => Response( data: htmlData, responseType: ResponseType.html, headers: headers, ); static Response file( String fileName, Uint8List bytes, { Map headers = const {}, }) => Response( data: {"fileName": fileName, "bytes": bytes}, responseType: ResponseType.streamFile, headers: headers, ); static Response sse( Stream eventStream, { int statusCode = HttpStatus.ok, Map headers = const {}, }) => Response( data: eventStream, responseType: ResponseType.sse, httpStatusCode: statusCode, headers: headers, ); static Response download( String fileName, Uint8List bytes, { Map headers = const {}, }) => Response( data: {"fileName": fileName, "bytes": bytes}, responseType: ResponseType.download, headers: headers, ); static Response back([String? key, String? message]) { String previousRoute = RouteHistory().previousRoute; if (key != null && message != null) { TemplateEngine().sessions[key] = message; } if (previousRoute.isNotEmpty) { return Response( responseType: ResponseType.redirect, data: previousRoute, httpStatusCode: HttpStatus.found, ); } return Response( responseType: ResponseType.redirect, data: RouteHistory().currentRoute, httpStatusCode: HttpStatus.found, ); } static Response backWithInput([String? input, String? message]) { String previousRoute = RouteHistory().previousRoute; if (input != null && message != null) { TemplateEngine().sessionErrors[input] = message; } if (previousRoute.isNotEmpty) { return Response( responseType: ResponseType.redirect, data: previousRoute, httpStatusCode: HttpStatus.found, ); } return Response( responseType: ResponseType.redirect, data: RouteHistory().currentRoute, httpStatusCode: HttpStatus.found, ); } } ================================================ FILE: lib/src/http/response/stream_file.dart ================================================ import 'dart:io'; import 'dart:typed_data'; import 'package:path/path.dart' as path; import 'package:mime/mime.dart'; class StreamFile { final String fileName; final Uint8List bytes; StreamFile({required this.fileName, required this.bytes}); ContentType? _contentType; Stream>? _stream; int _length = 0; ContentType get contentType => _contentType ?? ContentType('application', 'octet-stream'); Stream>? get stream => _stream; int get length => _length; String get contentDisposition => 'attachment; filename="${path.basename(fileName)}"'; StreamFile? call() { String mimeType = lookupMimeType(path.basename(fileName), headerBytes: bytes) ?? ""; String primaryType = mimeType.split('/').first; String subType = mimeType.split('/').last; _contentType = ContentType(primaryType, subType); _stream = Stream>.fromIterable([bytes]); _length = bytes.length; return this; } } ================================================ FILE: lib/src/http/session/session_file_store.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; import '../../cryptographic/vania_encryption.dart'; import 'package:vania/src/utils/helper.dart' show env; class SessionFileStore { static final SessionFileStore _singleton = SessionFileStore._internal(); factory SessionFileStore() => _singleton; SessionFileStore._internal(); final String _secretKey = env('APP_KEY'); final String sessionPath = 'storage/framework/sessions'; /// Stores session data in a file with the given session ID. /// /// This method creates or overwrites a file in the session path to store /// session data. The data is serialized to JSON, encrypted, and then written /// to the file. An expiration timestamp is added to the session data, which /// is set to the current time plus the specified duration. The file is locked /// during the write operation to ensure data integrity. /// /// Parameters: /// - [sessionId]: The unique identifier for the session. /// - [data]: A map containing the session data to be stored. /// - [duration]: (Optional) The duration for which the session is valid. /// Defaults to one hour. /// Future storeSession( String sessionId, Map data, { Duration duration = const Duration(hours: 1), }) async { sessionId = _makeHash(sessionId).toString(); final file = File('$sessionPath/$sessionId'); if (!await file.exists()) { await file.create(recursive: true); } int expiration = DateTime.now().toUtc().millisecondsSinceEpoch + duration.inMilliseconds; final Map sessionData = { "data": data, "expiration": expiration, }; final String content = await VaniaEncryption.encryptString( json.encode(sessionData), _secretKey, ); final raf = await file.open(mode: FileMode.write); try { await raf.writeFrom(utf8.encode(content)); } finally { try { await raf.unlock(); } catch (_) {} await raf.close(); } } /// Retrieves the session data associated with the given session ID. /// /// This function checks if a session file exists for the given session ID, /// reads its contents, and decrypts the data. If the session data is valid /// and not expired, it returns the session data as a map. If the session /// does not exist, is expired, or decryption fails, it returns null. /// /// Parameters: /// - [sessionId]: The unique identifier for the session to be retrieved. /// /// Returns: /// A map containing the session data, or null if the session does not exist, /// is expired, or if there is an error in reading or decrypting the file. /// Future?> retrieveSession(String sessionId) async { sessionId = _makeHash(sessionId).toString(); final file = File('$sessionPath/$sessionId'); if (!await file.exists()) { return null; } final raf = await file.open(mode: FileMode.read); String fileContent = ''; try { final int length = await raf.length(); final List bytes = await raf.read(length); fileContent = utf8.decode(bytes); } finally { try { await raf.unlock(); } catch (_) {} await raf.close(); } final String decrypted = await VaniaEncryption.decryptString( fileContent, _secretKey, ); if (decrypted.isEmpty) { return null; } final Map data = json.decode(decrypted); int expiration = int.tryParse(data['expiration'].toString()) ?? 0; if (!DateTime.now().toUtc().isBefore( DateTime.fromMillisecondsSinceEpoch(expiration), )) { await file.delete(); return null; } return data['data']; } Future hasSession(String sessionId) async { Map? data = await retrieveSession(sessionId); return data != null; } /// Deletes a specific session from the session storage. /// /// This function first checks if a session exists for the given session ID. /// If the session exists, it deletes the session by setting the expiration /// time to the current time minus 1 millisecond, and encrypts the new data /// using the `VaniaEncryption` class. The encrypted data is then written /// to the session file. If the session does not exist, this function does /// nothing. /// /// Parameters: /// - [sessionId]: The unique identifier for the session to be deleted. /// /// Returns: /// A Future that resolves to `null`, indicating that the session was /// successfully deleted. /// Future deleteSession(String sessionId) async { sessionId = _makeHash(sessionId).toString(); final file = File('$sessionPath/$sessionId'); if (await file.exists()) { final raf = await file.open(mode: FileMode.write); try { int expiration = DateTime.now().toUtc().millisecondsSinceEpoch - 1; final String content = await VaniaEncryption.encryptString( json.encode({"data": {}, "expiration": expiration}), _secretKey, ); await raf.writeFrom(utf8.encode(content)); } finally { try { await raf.unlock(); } catch (_) {} await raf.close(); } } } Digest _makeHash(String key) { var secKey = utf8.encode(_secretKey); var bytes = utf8.encode(key); var hmacSha256 = Hmac(sha256, secKey); return hmacSha256.convert(bytes); } } ================================================ FILE: lib/src/http/session/session_manager.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:vania/src/utils/functions.dart'; import 'package:vania/src/utils/helper.dart' show env; import 'session_file_store.dart'; class SessionManager { HttpRequest? _request; String sessionKey = '${env('APP_NAME', 'Vania')}_session'; String _csrfToken = ''; String get csrfToken => _csrfToken; Map _allSessions = {}; Map get allSessions => _allSessions; final Duration _sessionLifeTime = Duration( seconds: env('SESSION_LIFETIME', 9000), ); bool secureSession = env('SECURE_SESSION', true); /// Generates a new session ID. /// /// This method creates a 64-byte random key using a secure random number generator, /// and encodes it using base64 URL encoding to ensure a unique and safe session ID. /// /// Returns: /// A base64 URL encoded string representing the session ID. String _generateSessionId() { final keyBytes = randomString(length: 64, numbers: true); return base64Url.encode(utf8.encode(keyBytes)); } /// Creates a CSRF token and sets it as a secure cookie in the HTTP response. /// /// This function checks if an 'XSRF-TOKEN' cookie is already present in the /// request. If not, it generates a new random CSRF token along with an /// initialization vector (IV), and sets them as cookies in the response. The /// token and IV are also stored in the session for future validation. /// /// Parameters: /// - `request`: The incoming HTTP request containing the cookies. /// - `response`: The HTTP response where the CSRF token cookie will be added. /// /// The generated CSRF token is URL-safe and securely stored in the session with /// the specified session lifetime. The cookie is configured with security /// attributes such as domain, expiration, SameSite policy, and HTTP-only flag /// to mitigate CSRF attacks. Future createXsrfToken( HttpRequest request, HttpResponse response, ) async { Cookie requestCookie = request.cookies.firstWhere( (cookie) => cookie.name == 'XSRF-TOKEN', orElse: () => Cookie('XSRF-TOKEN', ''), ); if (requestCookie.value.isEmpty) { await _generateNewCsrfToken(response); } else { String? storedToken = _allSessions['x_csrf_token']; if (storedToken == null || storedToken.isEmpty) { await _generateNewCsrfToken(response); } else { _csrfToken = storedToken; } } } /// Generates a new CSRF token and stores it in the session and a secure cookie. /// /// This method generates a random CSRF token and initialization vector (IV), /// stores them in the session and a secure cookie, and sets the cookie in the /// response. The cookie is configured with security attributes such as /// expiration, SameSite policy, and HTTP-only flag to mitigate CSRF attacks. /// /// Parameters: /// - `response`: The HTTP response where the CSRF token cookie will be added. /// /// The generated CSRF token is URL-safe and securely stored in the session with /// the specified session lifetime. Future _generateNewCsrfToken(HttpResponse response) async { String token = randomString(length: 40, numbers: true); String iv = randomString(length: 32, numbers: true); await setSession('x_csrf_token', token); await setSession('x_csrf_iv', iv); _csrfToken = token; String cookieValue = _computeCsrfCookieValue(token, iv); Cookie cookie = Cookie('XSRF-TOKEN', cookieValue) ..expires = DateTime.now().add(Duration(seconds: 9000)) ..sameSite = SameSite.lax ..secure = secureSession ..path = '/' ..httpOnly = true; response.cookies.add(cookie); } /// Computes the value of the CSRF cookie for the given CSRF token and /// initialization vector (IV). /// The method uses the HMAC algorithm with SHA-512 to create a digest from /// the given token and IV. The digest is then encoded in Base64 and stored /// in the CSRF cookie in the response. The cookie is configured with /// security attributes such as expiration, SameSite policy, and HTTP-only flag /// to mitigate CSRF attacks. String _computeCsrfCookieValue(String token, String iv) { var hmac = Hmac(sha512, utf8.encode(iv)); final Digest digest = hmac.convert(utf8.encode(token)); return base64.encode( utf8.encode(jsonEncode({'token': base64.encode(digest.bytes)})), ); } /// Starts a new session or retrieves an existing session from the request. /// /// This method initializes a session for the given HTTP request and response. /// If a sessionKey cookie is already present in the request, its value is /// used as the session . Otherwise, a new session is generated and set /// as a cookie in the response. /// /// The session is stored in a cookie with properties configured for HTTP /// only access, insecure transmission (consider changing to true for secure /// transmission), a path set to '/', and an expiration set to the session /// timeout duration. /// /// Parameters: /// - [request]: The incoming HTTP request containing cookies. /// - [response]: The HTTP response where the session cookie will be added. /// /// Returns: /// A string representing the session. Future sessionStart(HttpRequest request, HttpResponse response) async { _request = null; _request ??= request; final cookie = request.cookies.firstWhere( (c) => c.name == sessionKey, orElse: () => Cookie(sessionKey, _generateSessionId()), ); String sessionId = cookie.value; response.cookies.add( Cookie(sessionKey, sessionId) ..httpOnly = true ..secure = secureSession ..path = '/' ..sameSite = SameSite.lax ..expires = DateTime.now().add(_sessionLifeTime), ); await _featchAllSessions(sessionId); await createXsrfToken(request, response); } String? getSessionId() { final cookie = _request?.cookies.firstWhere( (c) => c.name == sessionKey, orElse: () => Cookie(sessionKey, ''), ); return cookie?.value; } Future _featchAllSessions(String sessionId) async { _allSessions = await SessionFileStore().retrieveSession(sessionId) ?? {}; } Future getSession(String key) async { if (_allSessions.isEmpty) { final sessionId = getSessionId(); if (sessionId != null) { _allSessions = await SessionFileStore().retrieveSession(sessionId) ?? {}; } } if (_allSessions[key] == null) { return null as T; } if (T.toString() == 'int') { return int.tryParse(_allSessions[key].toString()) as T; } if (T.toString() == 'double') { return double.tryParse(_allSessions[key].toString()) as T; } if (T.toString() == 'bool') { return bool.tryParse(_allSessions[key].toString()) as T; } return _allSessions[key]; } /// Stores a value in the session data associated with the current session ID. /// /// If a session is found, it verifies the existence and validity of the session. /// If the session exists, it updates the session data by adding the given key-value pair, /// and saves the updated session data. If the session does not exist or is invalid, /// it does not store the value. /// Future setSession(String key, dynamic value) async { final sessionId = getSessionId(); if (sessionId != null) { Map? session = await SessionFileStore().retrieveSession( sessionId, ); if (session != null) { session.addAll({key: value}); } else { session = {key: value}; } _allSessions = session; await SessionFileStore().storeSession(sessionId, session); } } /// Deletes a specific key from the current session data. /// /// If a session is found, it verifies the existence and validity of the session. /// If the session exists, it removes the given key from the session data, and saves the /// updated session data. If the session does not exist or is invalid, it does not delete /// the key. /// /// Parameters: /// - [key]: The key to be deleted from the session data. Future deleteSession(String key) async { final String? sessionId = getSessionId(); if (sessionId != null) { Map? session = await SessionFileStore().retrieveSession( sessionId, ); if (session != null) { session.remove(key); _allSessions = session; await SessionFileStore().storeSession(sessionId, session); } } } Future destroyAllSessions() async { final sessionId = getSessionId(); if (sessionId != null) { _allSessions = {}; await SessionFileStore().storeSession(sessionId, {}); } } } ================================================ FILE: lib/src/http/validation/custom_validation_rule.dart ================================================ class CustomValidationRule { final String ruleName; final String message; final Future Function(Map, dynamic, String?) fn; CustomValidationRule({ required this.ruleName, required this.message, required this.fn, }); } ================================================ FILE: lib/src/http/validation/field_validation.dart ================================================ class FieldValidation { final String fieldName; final List _rules = []; final Map _messages = {}; FieldValidation(this.fieldName); Map get toMapMessages => _messages.map((key, message) { final parts = key.split('.*.'); final ruleName = parts.isNotEmpty ? parts.last : key; return MapEntry(ruleName, message); }); FieldValidation alpha({String? messages}) { _rules.add('alpha'); if (messages != null) { _messages['$fieldName.alpha'] = messages; } return this; } FieldValidation alphaDash({String? messages}) { _rules.add('alpha_dash'); if (messages != null) { _messages['$fieldName.alpha_dash'] = messages; } return this; } FieldValidation alphaNumeric({String? messages}) { _rules.add('alpha_numeric'); if (messages != null) { _messages['$fieldName.alpha_numeric'] = messages; } return this; } FieldValidation between(int first, int second, {String? messages}) { _rules.add('between:$first,$second'); if (messages != null) { _messages['$fieldName.between'] = messages; } return this; } FieldValidation boolean({String? messages}) { _rules.add('boolean'); if (messages != null) { _messages['$fieldName.boolean'] = messages; } return this; } FieldValidation confirmed({String? messages}) { _rules.add('confirmed'); if (messages != null) { _messages['$fieldName.confirmed'] = messages; } return this; } FieldValidation date({String? messages}) { _rules.add('date'); if (messages != null) { _messages['$fieldName.date'] = messages; } return this; } FieldValidation dateTime({String? messages}) { _rules.add('date_time'); if (messages != null) { _messages['$fieldName.date_time'] = messages; } return this; } FieldValidation email({String? messages}) { _rules.add('email'); if (messages != null) { _messages['$fieldName.email'] = messages; } return this; } FieldValidation endWith(String value, {String? messages}) { _rules.add('end_with:$value'); if (messages != null) { _messages['$fieldName.end_with'] = messages; } return this; } FieldValidation file(List types, {String? messages}) { final formatted = 'file:${types.join(',')}'; _rules.add(formatted); if (messages != null) { _messages['$fieldName.file'] = messages; } return this; } FieldValidation greaterThan(int value, {String? messages}) { _rules.add('greater_than:$value'); if (messages != null) { _messages['$fieldName.greater_than'] = messages; } return this; } FieldValidation image({String? messages}) { _rules.add('image'); if (messages != null) { _messages['$fieldName.image'] = messages; } return this; } FieldValidation integer({String? messages}) { _rules.add('integer'); if (messages != null) { _messages['$fieldName.integer'] = messages; } return this; } FieldValidation ip({String? messages}) { _rules.add('ip'); if (messages != null) { _messages['$fieldName.ip'] = messages; } return this; } FieldValidation isDouble({String? messages}) { _rules.add('double'); if (messages != null) { _messages['$fieldName.double'] = messages; } return this; } FieldValidation isIn(List value, {String? messages}) { _rules.add('in:${value.join(',')}'); if (messages != null) { _messages['$fieldName.in'] = messages; } return this; } FieldValidation isList({String? messages}) { _rules.add('array'); if (messages != null) { _messages['$fieldName.array'] = messages; } return this; } FieldValidation json({String? messages}) { _rules.add('json'); if (messages != null) { _messages['$fieldName.json'] = messages; } return this; } FieldValidation lengthBetween(int first, int second, {String? messages}) { _rules.add('length_between:$first,$second'); if (messages != null) { _messages['$fieldName.length_between'] = messages; } return this; } FieldValidation lessThan(int value, {String? messages}) { _rules.add('less_than:$value'); if (messages != null) { _messages['$fieldName.less_than'] = messages; } return this; } FieldValidation max(int value, {String? messages}) { _rules.add('max:$value'); if (messages != null) { _messages['$fieldName.max'] = messages; } return this; } FieldValidation maxLength(int value, {String? messages}) { _rules.add('max_length:$value'); if (messages != null) { _messages['$fieldName.max_length'] = messages; } return this; } FieldValidation min(int value, {String? messages}) { _rules.add('min:$value'); if (messages != null) { _messages['$fieldName.min'] = messages; } return this; } FieldValidation minLength(int value, {String? messages}) { _rules.add('min_length:$value'); if (messages != null) { _messages['$fieldName.min_length'] = messages; } return this; } FieldValidation notIn(List value, {String? messages}) { _rules.add('not_in:${value.join(',')}'); if (messages != null) { _messages['$fieldName.not_in'] = messages; } return this; } FieldValidation numeric({String? messages}) { _rules.add('numeric'); if (messages != null) { _messages['$fieldName.numeric'] = messages; } return this; } FieldValidation regExp(String rule, {String? messages}) { _rules.add('reg_exp:$rule'); if (messages != null) { _messages['$fieldName.reg_exp'] = messages; } return this; } FieldValidation required({String? messages}) { _rules.add('required'); if (messages != null) { _messages['$fieldName.required'] = messages; } return this; } FieldValidation requiredIf(List value, {String? messages}) { _rules.add('required_if:${value.join(',')}'); if (messages != null) { _messages['$fieldName.required_if'] = messages; } return this; } FieldValidation requiredIfNot(List value, {String? messages}) { _rules.add('required_if_not:${value.join(',')}'); if (messages != null) { _messages['$fieldName.required_if_not'] = messages; } return this; } FieldValidation startWith(String value, {String? messages}) { _rules.add('start_with:$value'); if (messages != null) { _messages['$fieldName.start_with'] = messages; } return this; } FieldValidation string({String? messages}) { _rules.add('string'); if (messages != null) { _messages['$fieldName.string'] = messages; } return this; } FieldValidation unique(String table, {String? messages}) { _rules.add('unique:$table,$fieldName'); if (messages != null) { _messages['$fieldName.unique'] = messages; } return this; } FieldValidation url({String? messages}) { _rules.add('url'); if (messages != null) { _messages['$fieldName.url'] = messages; } return this; } FieldValidation uuid({String? messages}) { _rules.add('uuid'); if (messages != null) { _messages['$fieldName.uuid'] = messages; } return this; } @override String toString() { return _rules.join('|'); } } ================================================ FILE: lib/src/http/validation/nested_validation.dart ================================================ import 'package:vania/src/extensions/map_extension.dart'; import 'validation_item.dart'; class NestedValidation { final String field; final Map data; final String rule; final List fieldsToValidate = []; NestedValidation({ required this.data, required this.field, required this.rule, }) { _process(); } void _process() { List parts = field.split('.*.'); /// eg. from products.*.{field} to products String mainField = parts[0]; /// getting list of products from data. List> list = data.getParam(mainField); /// remove main filed to loop and get final field to validate List fieldsExceptMainField = parts.sublist(1); _processNestedField(mainField, fieldsExceptMainField, rule, list); } void _processNestedField( String mainField, List fields, String rule, List> items, ) { items.asMap().forEach((int index, Map item) { String field = fields.first; String fieldNameWithPositionIndex = '$mainField.$index.$field'; /// this mean we already get field value to validate if (fields.length == 1) { dynamic value = item.getParam(field); fieldsToValidate.add( ValidationItem( field: fieldNameWithPositionIndex, name: field.split('.').last, value: value, rule: rule, ), ); } /// this mean we still need to get the actual field value else if (fields.length >= 2) { List fieldsExceptMainField = fields.sublist(1); List> newItems = item.getParam(field); _processNestedField( fieldNameWithPositionIndex, fieldsExceptMainField, rule, newItems, ); } }); } } ================================================ FILE: lib/src/http/validation/rules.dart ================================================ import 'package:vania/src/database/db.dart'; import 'package:vania/src/http/request/request_file.dart'; class Rules { /// Check field is required static bool isRequired( Map data, dynamic value, String args, ) { if (value == null) { return false; } if (value is List) { return value.isNotEmpty; } return value.toString().isNotEmpty; } /// Check field is email static bool isEmail(Map data, dynamic value, String args) { RegExp emailRegex = RegExp( r'^[\w-]+(\.[\w-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,})$', ); return emailRegex.hasMatch(value.toString()); } /// Check field is string static bool isString(Map data, dynamic value, String args) { return value is String; } /// Check field is number static bool isNumeric(Map data, dynamic value, String args) { try { num.parse(value.toString()); return true; } catch (e) { return false; } } /// Check field is ip address static bool isIp(Map data, dynamic value, String args) { RegExp ipAddressRegex = RegExp( r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', ); return ipAddressRegex.hasMatch(value.toString()); } /// Check field is boolean static bool isBoolean(Map data, dynamic value, String args) { try { bool.parse(value.toString()); return true; } catch (e) { return false; } } /// Check field is integer static bool isInteger(Map data, dynamic value, String args) { try { int.parse(value.toString()); return true; } catch (e) { return false; } } /// Check field is double static bool isDouble(Map data, dynamic value, String args) { try { double.parse(value.toString()); return true; } catch (e) { return false; } } /// Check field is array static bool isArray(Map data, dynamic value, String args) { return value is List; } /// Check field is map or json static bool isJson(Map data, dynamic value, String args) { return value is Map; } /// Check field is alphabetic static bool isAlpha(Map data, dynamic value, String args) { RegExp alphabeticRegex = RegExp(r'^[a-zA-Z]+$'); return alphabeticRegex.hasMatch(value.toString()); } /// Check field is only with alphabetic, dash or underscore static bool isAlphaDash( Map data, dynamic value, String args, ) { RegExp alphaDashRegex = RegExp(r'^[a-zA-Z-_]+$'); return alphaDashRegex.hasMatch(value.toString()); } /// Check field is only with alphabetic, number static bool isAlphaNumeric( Map data, dynamic value, String args, ) { RegExp alphaNumericRegex = RegExp(r'^[a-zA-Z0-9]+$'); return alphaNumericRegex.hasMatch(value.toString()); } /// Check field is a date or date time static bool isDate(Map data, dynamic value, String args) { try { DateTime? dateTime = DateTime.tryParse(value.toString()); return dateTime != null; } catch (e) { return false; } } /// Check field is a valid url static bool isUrl(Map data, dynamic value, String args) { try { Uri? uri = Uri.tryParse(value); return uri != null && uri.hasScheme && uri.hasAuthority; } catch (e) { return false; } } /// Check field is a valid uuid static bool isUUID(Map data, dynamic value, String args) { RegExp uuidRegex = RegExp( r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', ); return uuidRegex.hasMatch(value.toString()); } /// Check field character is given max length static bool maxLength(Map data, dynamic value, String max) { return value.toString().length <= num.parse(max.toString()); } /// Check field character is given min length static bool minLength(Map data, dynamic value, String min) { return value.toString().length >= num.parse(min.toString()); } /// Check field character is between given length static bool lengthBetween( Map data, dynamic value, String values, ) { List parts = values.toString().split(','); num value1 = num.parse(parts[0]); num value2 = num.parse(parts[1]); value = value.toString().length; if (value1 < value2) { return value >= value1 && value <= value2; } return value >= value2 && value <= value1; } /// Check field is unique static Future unique( Map data, dynamic value, String values, ) async { List parts = values.toString().split(','); String table = parts[0]; String column = parts[1]; final exists = await DB .table(table) .whereEqualTo(column, value) .doesntExist(); return exists; } /// Check field is between given value static bool between(Map data, dynamic value, String values) { List parts = values.toString().split(','); num value1 = num.parse(parts[0]); num value2 = num.parse(parts[1]); value = num.parse(value.toString()); if (value1 < value2) { return value >= value1 && value <= value2; } return value >= value2 && value <= value1; } /// Check field is greater than given value static bool greaterThan( Map data, dynamic value, String compare, ) { value = num.parse(value.toString()); return value > num.parse(compare.toString()); } /// Check field is less than given value static bool lessThan( Map data, dynamic value, String compare, ) { value = num.parse(value.toString()); return value < num.parse(compare.toString()); } /// Check field is reach min value static bool min(Map data, dynamic value, String compare) { value = num.parse(value.toString()); return value >= num.parse(compare.toString()); } /// Check field is not over max value static bool max(Map data, dynamic value, String compare) { value = num.parse(value.toString()); return value <= num.parse(compare.toString()); } /// Check field is in given array static bool inArray(Map data, dynamic value, String arr) { List array = arr.toString().split(','); return array.contains(value.toString()); } /// Check field is not in given array static bool notInArray(Map data, dynamic value, String arr) { List array = arr.toString().split(','); return !array.contains(value.toString()); } /// Check field start with given text static bool startWith( Map data, dynamic value, String start, ) { return value.toString().startsWith(start.toString()); } /// Check field end with given text static bool endWith(Map data, dynamic value, String end) { return value.toString().endsWith(end.toString()); } /// Check 2 password are matched static bool confirmed(Map data, dynamic value, String key) { key = key.toString().isEmpty ? 'password_confirmation' : key; dynamic confirmValue = data[key]; return confirmValue == value; } /// Check field is required when condition is matched static bool requiredIf( Map data, dynamic value, String payload, ) { List parts = payload.toString().split(','); String secondField = parts[0]; String secondFieldValueFromRule = parts[1].toString(); String? secondFieldValueFromRequest = data[secondField].toString(); /// Check only when req value and rule value are same if (secondFieldValueFromRule == secondFieldValueFromRequest) { return isRequired(data, value, ''); } return true; } /// Check field is required when condition is not matched static bool requiredIfNot( Map data, dynamic value, String payload, ) { List parts = payload.toString().split(','); String secondField = parts[0]; String secondFieldValueFromRule = parts[1].toString(); String? secondFieldValueFromRequest = data[secondField].toString(); /// Check only when req value and rule value are same if (secondFieldValueFromRule != secondFieldValueFromRequest) { return isRequired(data, value, ''); } return true; } /// Check field is valid image static bool isImage(Map data, dynamic value, String args) { if (value is! RequestFile) { return false; } List extensions = [ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff', 'ico', ]; if (args.toString().isNotEmpty) { extensions = args.toString().split(','); } if (extensions.contains(value.extension)) { return true; } return false; } /// Check field is a file /// not a file => false /// if added supported extension in validation, check with extension /// return true static bool isFile(Map data, dynamic value, String args) { if (value is! RequestFile && value is! List) { return false; } if (args.isEmpty) { return true; } List validExtensions = args.split(','); bool hasValidExtension(RequestFile file) { return validExtensions.contains(file.extension); } if (value is List) { return value.every(hasValidExtension); } else { return hasValidExtension(value); } } static bool regExp(Map data, dynamic value, String args) { RegExp regExp = RegExp(args); return regExp.hasMatch(value.toString()); } } ================================================ FILE: lib/src/http/validation/validation_chain/export_chain_validation.dart ================================================ export 'rules/between.dart'; export 'rules/confirmed.dart'; export 'rules/end_width.dart'; export 'rules/greater_than.dart'; export 'rules/in_array.dart'; export 'rules/is_alpha.dart'; export 'rules/is_alpha_dash.dart'; export 'rules/is_alpha_numeric.dart'; export 'rules/is_array.dart'; export 'rules/is_boolean.dart'; export 'rules/is_date.dart'; export 'rules/is_double.dart'; export 'rules/is_email.dart'; export 'rules/is_file.dart'; export 'rules/is_image.dart'; export 'rules/is_integer.dart'; export 'rules/is_ip.dart'; export 'rules/is_json.dart'; export 'rules/is_numeric.dart'; export 'rules/is_required.dart'; export 'rules/is_string.dart'; export 'rules/is_url.dart'; export 'rules/is_uuid.dart'; export 'rules/lenght_between.dart'; export 'rules/less_than.dart'; export 'rules/max.dart'; export 'rules/max_lenght.dart'; export 'rules/min_lenght.dart'; export 'rules/not_in_array.dart'; export 'rules/required_if.dart'; export 'rules/required_if_not.dart'; export 'rules/start_with.dart'; export 'validation.dart'; export 'validation_rule.dart'; ================================================ FILE: lib/src/http/validation/validation_chain/rules/between.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class Between extends ValidationRule { final num lowerBoundary; final num higherBoundary; Between({ required this.lowerBoundary, required this.higherBoundary, super.message, }); @override bool validate(value, data) { value = num.parse(value.toString()); return value >= lowerBoundary && value <= higherBoundary; } @override String getDefaultErrorMessage(String field) { return 'The $field should be between $lowerBoundary and $higherBoundary'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/confirmed.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class Confirmed extends ValidationRule { Confirmed({super.message}); @override bool validate(value, data) { dynamic confirmValue = data['password_confirmation']; return confirmValue == value; } @override String getDefaultErrorMessage(String field) { return 'Both passwords should match'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/end_width.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class EndWith extends ValidationRule { final String end; EndWith({required this.end, super.message}); @override bool validate(value, data) { return value.toString().endsWith(end.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field must end with $end'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/greater_than.dart ================================================ import 'dart:convert'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class GreaterThan extends ValidationRule { final num compare; GreaterThan({required this.compare, super.message}); @override bool validate(value, data) { value = num.parse(value.toString()); return value > num.parse(compare.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field be greater than $compare'; } GreaterThan copyWith({num? compare}) { return GreaterThan(compare: compare ?? this.compare); } Map toMap() { final result = {}; result.addAll({'compare': compare}); return result; } factory GreaterThan.fromMap(Map map) { return GreaterThan(compare: map['compare'] ?? 0); } String toJson() => json.encode(toMap()); factory GreaterThan.fromJson(String source) => GreaterThan.fromMap(json.decode(source)); @override String toString() => 'GreaterThan(compare: $compare)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is GreaterThan && other.compare == compare; } @override int get hashCode => compare.hashCode; } ================================================ FILE: lib/src/http/validation/validation_chain/rules/in_array.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class InArray extends ValidationRule { final List array; InArray({required this.array, super.message}); @override bool validate(value, data) { return array.contains(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field field have to a part of ${array.toString()}'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_alpha.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsAlpha extends ValidationRule { IsAlpha({super.message}); @override bool validate(value, data) { return value is String && alphaRegExp.hasMatch(value); } @override String getDefaultErrorMessage(String field) { return 'The $field must contains just alphabet words'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_alpha_dash.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsAlphaDash extends ValidationRule { IsAlphaDash({super.message}); @override bool validate(value, data) { return alphaDashRegExp.hasMatch(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field can just contains alphabet and also dash'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_alpha_numeric.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsAlphaNumeric extends ValidationRule { IsAlphaNumeric({super.message}); @override bool validate(value, data) { return alphaNumericRegExp.hasMatch(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field can just contains alphabet and also numbers'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_array.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsArray extends ValidationRule { IsArray({super.message}); @override bool validate(value, data) { return value is List; } @override String getDefaultErrorMessage(String field) { return 'The $field must be an array'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_boolean.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsBoolean extends ValidationRule { IsBoolean({super.message}); @override bool validate(value, data) { return value is bool; } @override String getDefaultErrorMessage(String field) { return 'The $field must be a boolean'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_date.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsDate extends ValidationRule { IsDate({super.message}); @override bool validate(value, data) { try { DateTime.parse(value.toString()); return true; } catch (_) { return false; } } @override String getDefaultErrorMessage(String field) { return 'The {field} must be a valid DateTime'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_double.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsDouble extends ValidationRule { IsDouble({super.message}); @override bool validate(value, data) { try { double.parse(value.toString()); return true; } catch (e) { return false; } } @override String getDefaultErrorMessage(String field) { return 'The $field must be a double'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_email.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsEmail extends ValidationRule { IsEmail({super.message}); @override bool validate(dynamic value, data) { return value != null && emailRegExp.hasMatch(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field must be a valid email'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_file.dart ================================================ import 'package:vania/src/http/request/request_file.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsFile extends ValidationRule { final String args; IsFile({required this.args, super.message}); @override bool validate(value, data) { if (value is! RequestFile && value is! List) { return false; } if (args.isEmpty) { return true; } List validExtensions = args.split(','); bool hasValidExtension(RequestFile file) { return validExtensions.contains(file.extension); } if (value is List) { return value.every(hasValidExtension); } else { return hasValidExtension(value); } } @override String getDefaultErrorMessage(String field) { return 'The $field must be a file'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_image.dart ================================================ import 'package:vania/src/http/request/request_file.dart'; import 'package:vania/src/http/validation/validation_chain/export_chain_validation.dart'; class IsImage extends ValidationRule { final String args; IsImage({required this.args, super.message}); @override bool validate(value, data) { if (value is RequestFile) { return false; } if (args.toString().isNotEmpty) { extensions = args.toString().split(','); } if (extensions.contains(value.extension)) { return true; } return false; } @override String getDefaultErrorMessage(String field) { return 'The $field must be a file'; } } List extensions = [ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'tiff', 'ico', ]; ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_integer.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsInteger extends ValidationRule { IsInteger({super.message}); @override bool validate(value, data) { try { int.parse(value.toString()); return true; } catch (_) { return false; } } @override String getDefaultErrorMessage(String field) { return 'The $field must be an integer'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_ip.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsIp extends ValidationRule { IsIp({super.message}); @override bool validate(value, data) { return ipRegExp.hasMatch(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field must be a valid IP address'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_json.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsJson extends ValidationRule { IsJson({super.message}); @override bool validate(value, data) { return value is Map; } @override String getDefaultErrorMessage(String field) { return 'The $field must be a Json'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_numeric.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsNumeric extends ValidationRule { IsNumeric({super.message}); @override bool validate(value, data) { try { num.parse(value.toString()); return true; } catch (_) { return false; } } @override String getDefaultErrorMessage(String field) { return 'The $field must be a number'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_required.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsRequired extends ValidationRule { IsRequired({super.message}); @override bool validate(dynamic value, data) { return value != null && value.toString().isNotEmpty; } @override String getDefaultErrorMessage(String field) { return 'The $field is required'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_string.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsString extends ValidationRule { IsString({super.message}); @override bool validate(value, data) { return value != null && value is String; } @override String getDefaultErrorMessage(String field) { return 'The $field must be a string'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_url.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsURL extends ValidationRule { IsURL({super.message}); @override bool validate(value, data) { try { Uri? uri = Uri.tryParse(value); return uri != null && uri.hasScheme && uri.hasAuthority; } catch (e) { return false; } } @override String getDefaultErrorMessage(String field) { return 'The {field} must be a valid UUID'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/is_uuid.dart ================================================ import 'package:vania/src/config/defined_regexp.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class IsUUID extends ValidationRule { IsUUID({super.message}); @override bool validate(value, data) { return uuidRegExp.hasMatch(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The {field} must be a valid UUID'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/lenght_between.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class LengthBetween extends ValidationRule { final num lowerBoundary; final num higherBoundary; LengthBetween({ super.message, required this.lowerBoundary, required this.higherBoundary, }); @override bool validate(value, data) { value = value.toString().length; return value >= lowerBoundary && value <= higherBoundary; } @override String getDefaultErrorMessage(String field) { return 'The $field should between $lowerBoundary and $higherBoundary'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/less_than.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class LessThan extends ValidationRule { final num compare; LessThan({required this.compare, super.message}); @override bool validate(value, data) { value = num.parse(value.toString()); return value < num.parse(compare.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field be less than $compare'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/max.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class Max extends ValidationRule { final num maxValue; Max({required this.maxValue, super.message}); @override bool validate(value, data) { value = num.parse(value.toString()); return value <= num.parse(maxValue.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field must be less than or equal $maxValue'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/max_lenght.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class MaxLength extends ValidationRule { int maxLength; MaxLength({required this.maxLength, super.message}); @override bool validate(value, data) { value = value.toString().length; return value.toString().length <= maxLength; } @override String getDefaultErrorMessage(String field) { return 'The {field} should not be greater than $maxLength character'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/min.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class Min extends ValidationRule { final num minValue; Min({required this.minValue, super.message}); @override bool validate(value, data) { value = num.parse(value.toString()); return value >= num.parse(minValue.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field be greater than or equal $minValue'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/min_lenght.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class MinLength extends ValidationRule { int minLength; MinLength({required this.minLength, super.message}); @override bool validate(value, data) { value = value.toString().length; return value.toString().length >= minLength; } @override String getDefaultErrorMessage(String field) { return 'The {field} should not be less than $minLength character'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/not_in_array.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class NotInArray extends ValidationRule { final List array; NotInArray({required this.array, super.message}); @override bool validate(value, data) { return !array.contains(value.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field field cannot be in ${array.toString()}'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/required_if.dart ================================================ import 'package:vania/src/http/validation/validation_chain/rules/is_required.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class RequiredIf extends ValidationRule { final String payload; RequiredIf({required this.payload, super.message}); @override bool validate(value, data) { List parts = payload.toString().split(','); String secondField = parts[0]; String secondFieldValueFromRule = parts[1].toString(); String? secondFieldValueFromRequest = data[secondField].toString(); /// check only when req value and rule value are same if (secondFieldValueFromRule != secondFieldValueFromRequest) { return IsRequired().validate(value, data); } return true; } @override String getDefaultErrorMessage(String field) { return 'The $field is Required'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/required_if_not.dart ================================================ import 'package:vania/src/http/validation/validation_chain/rules/is_required.dart'; import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class RequiredIfNot extends ValidationRule { final String payload; RequiredIfNot({required this.payload, super.message}); @override bool validate(value, data) { List parts = payload.toString().split(','); String secondField = parts[0]; String secondFieldValueFromRule = parts[1].toString(); String? secondFieldValueFromRequest = data[secondField].toString(); /// check only when req value and rule value are same if (secondFieldValueFromRule != secondFieldValueFromRequest) { return IsRequired().validate(value, data); } return true; } @override String getDefaultErrorMessage(String field) { return 'The $field is Required'; } } ================================================ FILE: lib/src/http/validation/validation_chain/rules/start_with.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class StartWith extends ValidationRule { final String start; StartWith({required this.start, super.message}); @override bool validate(value, data) { return value.toString().startsWith(start.toString()); } @override String getDefaultErrorMessage(String field) { return 'The $field must start with $start'; } } ================================================ FILE: lib/src/http/validation/validation_chain/validation.dart ================================================ import 'package:vania/src/http/validation/validation_chain/validation_rule.dart'; class Validation { final String field; final List rules; const Validation({required this.field, required this.rules}); } ================================================ FILE: lib/src/http/validation/validation_chain/validation_rule.dart ================================================ abstract class ValidationRule { final String? message; ValidationRule({this.message}); bool validate(dynamic value, Map data); String get errorMessage => message ?? getDefaultErrorMessage(''); String getDefaultErrorMessage(String field); } ================================================ FILE: lib/src/http/validation/validation_item.dart ================================================ class ValidationItem { final String field; final String name; final String rule; final dynamic value; const ValidationItem({ required this.field, required this.name, required this.rule, this.value, }); } ================================================ FILE: lib/src/http/validation/validator.dart ================================================ import 'package:sprintf/sprintf.dart'; import 'package:vania/src/extensions/string_list_extension.dart'; import 'package:vania/src/extensions/map_extension.dart'; import 'custom_validation_rule.dart'; import 'nested_validation.dart'; import 'rules.dart'; import 'validation_item.dart'; class Validator { /// request data final Map data; Validator({required this.data}); final Map _errors = {}; List get _methodNoNeedToSplitArguments => ['in']; /// check validation has errors bool get hasError => _errors.isNotEmpty; /// get list of error messages Map get errors => _errors; /// Add custom validation rules to the validator. /// /// This method allows the addition of custom validation rules to the /// validator. Each rule is represented by a [CustomValidationRule] object, /// which includes the rule's name, error message, and validation function. /// /// The rule's name is used as a key in the internal [_matchingRules] map, /// and the rule's message and function are stored as values. This allows /// for custom validation logic to be applied during the validation process. /// /// [rules] is a list of [CustomValidationRule] objects to be added. /// void customRule(List rules) { for (CustomValidationRule rule in rules) { _matchingRules[rule.ruleName] = { 'message': rule.message, 'function': rule.fn, }; } } /// set custom validator messages /// ``` /// validator.setNewMessages({'required': 'The {field} is required}); /// validator.setNewMessages({'name.required': 'The name is required}); /// ``` void setNewMessages(Map messages) { messages.forEach((String key, String value) { List splitedKey = key.split('.'); if (splitedKey.length > 1) { Function function = _matchingRules[splitedKey[1]]?['function']; _matchingRules[key] = { 'message': value, 'function': function, }; } else if (_matchingRules[key] != null) { _matchingRules[key]?['message'] = value; } }); } bool _isNestedValidation(String field) { return field.contains('.*.'); } /// validate your data /// ``` /// validator.validate({'field' : 'required|string'}); /// ``` Future validate(Map rules) async { for (var entry in rules.entries) { String field = entry.key; String rule = entry.value; if (_isNestedValidation(field)) { NestedValidation v = NestedValidation( data: data, field: field, rule: rule, ); for (ValidationItem item in v.fieldsToValidate) { await _validateItem(item); } } else { await _validateItem( ValidationItem( field: field, name: field.split('.').last, value: data.getParam(field), rule: rule, ), ); } } } Future _validateItem(ValidationItem item) async { if (item.value == null && !item.rule.contains('required')) { return; } List rulesForEachName = item.rule.split('|'); for (String rule in rulesForEachName) { String? error = await _applyMatchingRule( item.field, item.name, item.value, rule, ); if (error != null) { _errors[item.field] = error; break; } } } Future _applyMatchingRule( String field, String name, dynamic value, String rule, ) async { List parts = rule.split(':'); String ruleKey = parts.first.toString().toLowerCase(); String args = parts.length >= 2 ? parts[1] : ''; Map? match = _matchingRules["$name.$ruleKey"] ?? _matchingRules[ruleKey]; if (match == null) { return null; } var result = Function.apply(match['function'], [ data, value, args, ]); if (result is Future) { result = await result; } if (result == true) { return null; } String error = match['message'] .toString() .replaceAll('{field}', name) .replaceAll('{value}', value == null ? '' : value.toString()); if (args.isNotEmpty) { List arguments = _methodNoNeedToSplitArguments.contains(ruleKey) ? [args.split(',').joinWithAnd()] : args.split(','); return sprintf(error, arguments); } return error; } final Map> _matchingRules = >{ 'required': { 'message': 'The {field} is required', 'function': Rules.isRequired, }, 'email': { 'message': 'The {field} is not a valid email', 'function': Rules.isEmail, }, 'string': { 'message': 'The {field} must be a string', 'function': Rules.isString, }, 'numeric': { 'message': 'The {field} must be a number', 'function': Rules.isNumeric, }, 'ip': { 'message': 'The {field} must be an ip address', 'function': Rules.isIp, }, 'boolean': { 'message': 'The {field} must be a boolean', 'function': Rules.isBoolean, }, 'integer': { 'message': 'The {field} must be an integer', 'function': Rules.isInteger, }, 'double': { 'message': 'The {field} must be a double', 'function': Rules.isDouble, }, 'array': { 'message': 'The {field} must be an array', 'function': Rules.isArray, }, 'json': { 'message': 'The {field} is not a valid json', 'function': Rules.isJson, }, 'alpha': { 'message': 'The {field} must be an alphabetic', 'function': Rules.isAlpha, }, 'alpha_dash': { 'message': 'The {field} must be only alphabetic and dash', 'function': Rules.isAlphaDash, }, 'alpha_numeric': { 'message': 'The {field} must be only alphabetic and number', 'function': Rules.isAlphaNumeric, }, 'date': { 'message': 'The {field} must be a date', 'function': Rules.isDate, }, 'url': { 'message': 'The {field} must be a url', 'function': Rules.isUrl, }, 'uuid': { 'message': 'The {field} is invalid uuid', 'function': Rules.isUUID, }, 'min_length': { 'message': 'The {field} must be at least %s character', 'function': Rules.minLength, }, 'max_length': { 'message': 'The {field} may not be greater than %s character', 'function': Rules.maxLength, }, 'length_between': { 'message': 'The {field} must be between %s and %s character', 'function': Rules.lengthBetween, }, 'between': { 'message': 'The {field} must be between %s and %s', 'function': Rules.between, }, 'in': { 'message': 'The selected {field} is invalid. Valid options are %s', 'function': Rules.inArray, }, 'not_in': { 'message': 'The {field} field cannot be {value}', 'function': Rules.notInArray, }, 'start_with': { 'message': 'The {field} must start with %s', 'function': Rules.startWith, }, 'end_with': { 'message': 'The {field} must end with %s', 'function': Rules.endWith, }, 'greater_than': { 'message': 'The {field} must be greater than %s', 'function': Rules.greaterThan, }, 'less_than': { 'message': 'The {field} must be less than %s', 'function': Rules.lessThan, }, 'min': { 'message': 'The {field} must be greater than or equal %s', 'function': Rules.min, }, 'max': { 'message': 'The {field} must be less than or equal %s', 'function': Rules.max, }, 'confirmed': { 'message': 'The two password did not match', 'function': Rules.confirmed, }, 'required_if': { 'message': 'The {field} is required', 'function': Rules.requiredIf, }, 'required_if_not': { 'message': 'The {field} is required', 'function': Rules.requiredIfNot, }, 'image': { 'message': 'The {field} is either invalid or unsupported extension', 'function': Rules.isImage, }, 'file': { 'message': 'The {field} is either invalid or unsupported extension', 'function': Rules.isFile, }, 'reg_exp': { 'message': 'The {field} is either invalid or unsupported extension', 'function': Rules.regExp, }, 'unique': { 'message': 'This {field} is exist', 'function': Rules.unique, }, }; } ================================================ FILE: lib/src/ioc_container.dart ================================================ typedef FactoryFunc = T Function(); class IoCContainer { static final IoCContainer _instance = IoCContainer._internal(); factory IoCContainer() => _instance; IoCContainer._internal(); final Map _singletons = {}; final Map> _factories = {}; void register(FactoryFunc factory, {bool singleton = false}) { if (singleton) { _singletons[T] = factory(); } else { _factories[T] = factory; } } T resolve() { if (_singletons.containsKey(T)) { return _singletons[T]; } else if (_factories.containsKey(T)) { return _factories[T]!() as T; } throw Exception( 'Service of type $T is not registered in the IoC container.', ); } } ================================================ FILE: lib/src/localization_handler/localization.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:vania/src/utils/helper.dart' show env; class Localization { static final Localization _singleton = Localization._internal(); factory Localization() { return _singleton; } Localization._internal(); String? _locale = env('APP_LOCALE'); final Map _language = {}; void setLocale(String locale) => _locale = locale; bool isLocale(String locale) => _locale == locale; /// Initializes the language data by loading all `.json` language files from `lib/lang` directory. /// - `LANG_PATH` specifies the directory where the language files are stored (defaults to `lib/lang/` if not set). /// - `LOCALE` specifies the language/locale to load (defaults to `en` if not set). void init() async { Directory languagePath = Directory(env('APP_LANG_PATH', 'lib/lang')); if (languagePath.existsSync()) { for (var entity in languagePath.listSync( recursive: true, followLinks: false, )) { if (entity is Directory) { final segments = entity.uri.pathSegments.where((s) => s.isNotEmpty); final subdirName = segments.last.toLowerCase(); final fileMap = {}; for (var file in entity .listSync(recursive: false) .whereType() .where((f) => f.path.toLowerCase().endsWith('.json'))) { try { final content = file.readAsStringSync(); final decoded = json.decode(content); fileMap.addAll(decoded); } catch (e) { stderr.writeln('⚠️ Failed to parse ${file.path}: $e'); } } _language[subdirName] = fileMap; } } } } /// Translates a string based on the provided key and optional arguments. String trans(String key, [Map? args, String? locale]) { if (!_language[locale ?? _locale].containsKey(key)) { return 'Translation not found for key: $key'; } String tmp = _language[locale ?? _locale][key]; if (args == null || args.isEmpty) { return tmp; } args.forEach((placeholder, value) { tmp = tmp.replaceAll('{$placeholder}', value.toString()); }); return tmp; } } ================================================ FILE: lib/src/logger/logger.dart ================================================ // ignore_for_file: constant_identifier_names import 'dart:io'; import '../utils/helper.dart'; class Logger { static const EMERGENCY = 'EMERGENCY'; static const ALERT = 'ALERT'; static const CRITICAL = 'CRITICAL'; static const ERROR = 'ERROR'; static const SUCCESS = 'SUCCESS'; static const WARNING = 'WARNING'; static const NOTICE = 'NOTICE'; static const INFO = 'INFO'; static const DEBUG = 'DEBUG'; static void log( String content, { String type = INFO, String fileName = 'vania', }) { final now = DateTime.now(); final directory = Directory(storagePath('logs')); if (!directory.existsSync()) { directory.createSync(recursive: true); } final logFile = File('${directory.path}/$fileName.log'); final fsSink = logFile.openWrite(mode: FileMode.append); var text = "[${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}]"; text += ' $type: '; text += content; text += '\n'; fsSink.write(text); fsSink.close(); } } ================================================ FILE: lib/src/mail/content.dart ================================================ class Content { /// The Blade view that represents the text version of the message. final String? text; /// The Blade view that should be rendered for the mailable. final String? html; const Content({this.text, this.html}); } ================================================ FILE: lib/src/mail/envelope.dart ================================================ import 'package:mailer/mailer.dart'; class Envelope { ///The address sending the message. final Address? from; /// The recipients of the message. final List
to; /// The recipients receiving a copy of the message. final List? cc; /// The recipients receiving a blind copy of the message. final List? bcc; /// The subject of the message. final String subject; Envelope({ this.from, required this.to, required this.subject, this.cc, this.bcc, }); } ================================================ FILE: lib/src/mail/mail.dart ================================================ import 'package:mailer/mailer.dart'; import 'content.dart'; import 'envelope.dart'; import 'mail_view.dart'; abstract class Mail { const Mail(); Content? content(); MailView? view(); Envelope envelope(); List? attachments(); } ================================================ FILE: lib/src/mail/mail_view.dart ================================================ class MailView { final String view; final Map? data; const MailView({required this.view, this.data}); } ================================================ FILE: lib/src/mail/mailable.dart ================================================ import 'dart:io'; import 'package:mailer/mailer.dart' as mailer; import 'package:mailer/mailer.dart'; import 'package:mailer/smtp_server.dart'; import 'package:meta/meta.dart'; import 'package:vania/src/mail/content.dart'; import 'package:vania/src/mail/envelope.dart'; import 'package:vania/src/mail/mail.dart'; import 'package:vania/src/utils/helper.dart' show env; import 'package:vania/src/view_engine/template_engine.dart'; import 'mail_view.dart'; @immutable class Mailable implements Mail { const Mailable(); SmtpServer _setupSmtpServer() { switch (env('MAIL_MAILER', 'smtp')) { case 'gmail': return gmail( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); case 'gmailSaslXoauth2': return gmailSaslXoauth2( env('MAIL_USERNAME', ''), env('MAIL_ACCESS_TOKEN', ''), ); case 'gmailRelaySaslXoauth2': return gmail( env('MAIL_USERNAME', ''), env('MAIL_ACCESS_TOKEN', ''), ); case 'hotmail': return hotmail( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); case 'mailgun': return mailgun( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); case 'qq': return qq( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); case 'yahoo': return yahoo( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); case 'yandex': return yandex( env('MAIL_USERNAME', ''), env('MAIL_PASSWORD', ''), ); default: return SmtpServer( env('MAIL_HOST', ''), username: env('MAIL_USERNAME', ''), password: env('MAIL_PASSWORD', ''), port: env('MAIL_PORT', 465), ssl: env('MAIL_ENCRYPTION', true), ignoreBadCertificate: env('MAIL_IGNORE_BAD_CERTIFICATE', true), ); } } Future send() async { final message = mailer.Message(); message.from = envelope().from ?? Address( env('MAIL_FROM_ADDRESS', ''), env('MAIL_FROM_NAME', ''), ); message.recipients.addAll(envelope().to); if (envelope().cc != null) { message.ccRecipients.addAll(envelope().cc!); } if (envelope().bcc != null) { message.ccRecipients.addAll(envelope().bcc!); } message.subject = envelope().subject; MailView? mailView = view(); Content? contentData = content(); if (mailView != null) { message.html = TemplateEngine().render( mailView.view, mailView.data ?? {}, ); } else if (contentData != null) { message.text = contentData.text; message.html = contentData.html; } if (attachments() != null) { message.attachments.addAll(attachments()!); } try { mailer.SendReport sendReport = await mailer.send( message, _setupSmtpServer(), ); return sendReport; } on SmtpMessageValidationException catch (e) { stderr.writeln( 'Failed to send email:${e.problems.map((error) => {message: error.msg, error: error.code}).toList()}', ); throw Exception( e.problems .map((error) => {message: error.msg, error: error.code}) .toList(), ); } catch (e) { stderr.writeln('Failed to send email: $e'); rethrow; } } @mustBeOverridden @override List? attachments() { throw UnimplementedError(); } @mustBeOverridden @override MailView? view() { throw UnimplementedError(); } @mustBeOverridden @override Content? content() { throw UnimplementedError(); } @mustBeOverridden @override Envelope envelope() { throw UnimplementedError(); } } ================================================ FILE: lib/src/redis/command/client.dart ================================================ import 'dart:convert'; import 'package:vania/src/logger/logger.dart'; import 'package:vania/src/redis/exception.dart'; import 'package:vania/src/redis/lowlevel/protocol_client.dart'; import 'package:vania/src/redis/lowlevel/resp.dart'; import 'package:vania/src/redis/vania_redis.dart'; class MultiCodec { final List codecs = [ RedisCodec(encoder: StringEncoder(), decoder: StringDecoder()), RedisCodec(encoder: IntEncoder(), decoder: IntDecoder()), ]; String encode(T value) { for (final e in codecs) { if (e.encoder.isSupporting(value)) { return e.encoder.convert(value); } } throw RedisConvertException('no encoder found'); } T decode(String value) { for (final e in codecs) { if (e.decoder.isSupporting(value)) { return e.decoder.convert(value); } } throw RedisConvertException('no decoder found'); } void registerCodec(RedisCodec codec) { codecs.add(codec); } } /// All commands type inherited abstract class Commands implements KeysCommands, ListCommands, TransactionCommands, PubSubCommands {} /// Implementation of [Commands] class CommandsClient implements Commands { final RedisProtocolClient _connection; CommandsClient._(this._connection); /// key type codecs final MultiCodec keyCodec = MultiCodec(); /// value type codecs final MultiCodec valueCodec = MultiCodec(); @override Future del(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['DEL', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.isInteger && res.integerValue == 1; } // key-value operation(String) commands @override Future exists(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['EXISTS', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.isInteger && res.integerValue == 1; } @override Future expire(K key, Duration duration) async { final keyString = keyCodec.encode(key); _connection.sendCommand( Resp(['EXPIRE', keyString, '${duration.inSeconds}']), ); final res = await _connection.receive(); res.throwIfError(); return res.isInteger && res.integerValue == 1; } @override Future getdel(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['GETDEL', keyString])); final res = await _connection.receive(); res.throwIfError(); final str = res.stringValue; if (str == null) { return null; } return valueCodec.decode(str); } @override Future get(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['GET', keyString])); final res = await _connection.receive(); res.throwIfError(); final str = res.stringValue; if (str == null) { return null; } return valueCodec.decode(str); } @override Future ttl(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['ttl', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future> keys(String pattern) async { _connection.sendCommand(Resp(['KEYS', pattern])); final res = await _connection.receive(); res.throwIfError(); final l = res.arrayValue; if (l == null) { return []; } return l.map((e) => e.toString()).toList(); } @override Future set(K key, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand(Resp(['SET', keyString, valueString])); final res = await _connection.receive(); res.throwIfError(); final s = res.stringValue; if (s == null) { return false; } return s == 'OK'; } @override Future setEx(K key, int ttl, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand( Resp(['SETEX', keyString, ttl.toString(), valueString]), ); final res = await _connection.receive(); res.throwIfError(); final s = res.stringValue; if (s == null) { return false; } return s == 'OK'; } @override Future append(K key, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand(Resp(['APPEND', keyString, valueString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future bitCount(K key, {int? start, int? end}) async { final keyString = keyCodec.encode(key); final command = ['BITCOUNT', keyString]; if (start != null && end != null) { command.addAll([start.toString(), end.toString()]); } _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future bitOp(String operation, K destKey, List keys) async { final destKeyString = keyCodec.encode(destKey); final keyStrings = keys.map((k) => keyCodec.encode(k)).toList(); final command = ['BITOP', operation, destKeyString, ...keyStrings]; _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future bitPos(K key, int bit, {int? start, int? end}) async { final keyString = keyCodec.encode(key); final command = ['BITPOS', keyString, bit.toString()]; if (start != null && end != null) { command.addAll([start.toString(), end.toString()]); } _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future decr(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['DECR', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future decrBy(K key, int decrement) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['DECRBY', keyString, decrement.toString()])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future getBit(K key, int offset) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['GETBIT', keyString, offset.toString()])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future getRange(K key, int start, int end) async { final keyString = keyCodec.encode(key); _connection.sendCommand( Resp(['GETRANGE', keyString, start.toString(), end.toString()]), ); final res = await _connection.receive(); res.throwIfError(); final str = res.stringValue; if (str == null) { return null; } return valueCodec.decode(str); } @override Future getSet(K key, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand(Resp(['GETSET', keyString, valueString])); final res = await _connection.receive(); res.throwIfError(); final str = res.stringValue; if (str == null) { return null; } return valueCodec.decode(str); } @override Future incr(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['INCR', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future incrBy(K key, int increment) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['INCRBY', keyString, increment.toString()])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future incrByFloat(K key, double increment) async { final keyString = keyCodec.encode(key); _connection.sendCommand( Resp(['INCRBYFLOAT', keyString, increment.toString()]), ); final res = await _connection.receive(); res.throwIfError(); return res.doubleValue; } @override Future> mGet(List keys) async { final keyStrings = keys.map((k) => keyCodec.encode(k)).toList(); _connection.sendCommand(Resp(['MGET', ...keyStrings])); final res = await _connection.receive(); res.throwIfError(); final l = res.arrayValue; if (l == null) { return []; } return l.map((e) => valueCodec.decode(e)).toList(); } @override Future mSet(Map keyValues) async { final command = ['MSET']; keyValues.forEach((k, v) { command.addAll([keyCodec.encode(k), valueCodec.encode(v)]); }); _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.stringValue == 'OK'; } @override Future mSetNX(Map keyValues) async { final command = ['MSETNX']; keyValues.forEach((k, v) { command.addAll([keyCodec.encode(k), valueCodec.encode(v)]); }); _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.integerValue == 1; } @override Future setBit(K key, int offset, int value) async { final keyString = keyCodec.encode(key); _connection.sendCommand( Resp(['SETBIT', keyString, offset.toString(), value.toString()]), ); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future pSetEx(K key, int ttl, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand( Resp(['PSETEX', keyString, ttl.toString(), valueString]), ); final res = await _connection.receive(); res.throwIfError(); final s = res.stringValue; if (s == null) { return false; } return s == 'OK'; } @override Future setNx(K key, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand(Resp(['SETNX', keyString, valueString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue == 1; } @override Future setRange(K key, int offset, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand( Resp(['SETRANGE', keyString, offset.toString(), valueString]), ); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future strlen(K key) async { final keyString = keyCodec.encode(key); _connection.sendCommand(Resp(['STRLEN', keyString])); final res = await _connection.receive(); res.throwIfError(); return res.integerValue; } @override Future setOption(String option) async { _connection.sendCommand(Resp(['CONFIG', 'SET', option])); final res = await _connection.receive(); res.throwIfError(); return res.stringValue == 'OK'; } @override Future getOption(String option) async { _connection.sendCommand(Resp(['CONFIG', 'GET', option])); final res = await _connection.receive(); res.throwIfError(); final l = res.arrayValue; if (l == null || l.isEmpty) { return null; } return l.last.stringValue; } // List operation commands @override Future> lrange(K key, int startIndex, int endIndex) async { final keyString = keyCodec.encode(key); _connection.sendCommand( Resp(['LRANGE', keyString, startIndex.toString(), endIndex.toString()]), ); final res = await _connection.receive(); res.throwIfError(); final l = res.arrayValue; if (l == null) { return []; } return l.map((e) => valueCodec.decode(e)).toList(); } @override Future rpush(K key, List values) async { final keyString = keyCodec.encode(key); final command = ['RPUSH', keyString]; command.addAll(values.map((e) => valueCodec.encode(e))); _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.isInteger; } @override Future lpush(K key, List values) async { final keyString = keyCodec.encode(key); final command = ['LPUSH', keyString]; command.addAll(values.map((e) => valueCodec.encode(e))); _connection.sendCommand(Resp(command)); final res = await _connection.receive(); res.throwIfError(); return res.isInteger; } @override Future lset(K key, int index, V value) async { final keyString = keyCodec.encode(key); final valueString = valueCodec.encode(value); _connection.sendCommand( Resp(['LSET', keyString, index.toString(), valueString]), ); final res = await _connection.receive(); res.throwIfError(); return res.stringValue == 'OK'; } // Transaction Commands @override Future exec() async { _connection.sendCommand(Resp(['EXEC'])); await _connection.receive(); } @override Future multi() async { _connection.sendCommand(Resp(['MULTI'])); await _connection.receive(); } @override Future discard() async { _connection.sendCommand(Resp(['DISCARD'])); await _connection.receive(); } // pubsub operation commands @override Stream psubscribe(String pattern) { _connection.sendCommand(Resp(['PSUBSCRIBE', pattern])); return _connection.stream .map((event) => event?.arrayValue) .where((event) => event != null) .map((event) => event!) .where((e) => e.length == 4) // length == 3 is subscribed notification .map((event) => valueCodec.decode(event.last)); } @override Future publish(String channel, V message) async { final messageString = valueCodec.encode(message); _connection.sendCommand(Resp(['PUBLISH', channel, messageString])); final res = await _connection.receive(); return res.integerValue; } Future auth({String? username, required String password}) async { if (username == null) { _connection.sendCommand(Resp(['AUTH', password])); } else { _connection.sendCommand(Resp(['AUTH', username, password])); } final res = await _connection.receive(); return res.stringValue; } } /// Redis Client class RedisClient { final RedisProtocolClient _connection; RedisClient._(this._connection); /// Establish connection to Redis server and send SELECT command static Future connect( String host, int port, { required int db, String? username, String? password, }) async { final rpc = await RedisProtocolClient.createConnection( host: host, port: port, ); if (password != null) { await _auth(rpc, username: username, password: password); } rpc.sendCommand(Resp(['SELECT', '$db'])); final res = await rpc.receive(); res.throwIfError(); return RedisClient._(rpc); } static Future _auth( RedisProtocolClient connection, { String? username, required String password, }) async { if (username == null) { connection.sendCommand(Resp(['AUTH', password])); } else { connection.sendCommand(Resp(['AUTH', username, password])); } final res = await connection.receive(); try { res.throwIfError(); } on RedisException catch (e) { Logger.log( jsonEncode({'error': 'RedisException', 'message': e.message}), type: Logger.ERROR, ); } return res.stringValue; } /// Get [Commands] /// [K] : key type /// [V] : value type Commands getCommands() => CommandsClient._(_connection); /// close connection Future close() => _connection.close(); } ================================================ FILE: lib/src/redis/command/codec.dart ================================================ import 'dart:convert'; /// converter base class /// convert [S] to [D] abstract class RedisConverter extends Converter { bool isSupporting(dynamic value) => value is S && U == D; } /// convert to String from [T] typedef RedisEncoder = RedisConverter; /// convert to [T] from String typedef RedisDecoder = RedisConverter; /// encoder and decoder pair class RedisCodec { final RedisEncoder encoder; final RedisDecoder decoder; RedisCodec({required this.encoder, required this.decoder}); } /// builtin String to String encoder class StringEncoder extends RedisEncoder { @override String convert(String input) => input; } /// builtin String to String decoder class StringDecoder extends RedisDecoder { @override String convert(String input) => input; } /// builtin int to String encoder class IntEncoder extends RedisEncoder { @override String convert(int input) => input.toString(); } /// builtin String to int decoder class IntDecoder extends RedisDecoder { @override int convert(String input) => int.parse(input); } ================================================ FILE: lib/src/redis/command/commands.dart ================================================ /// key-value operation commands abstract class KeysCommands { /// DEL command (delete item) Future del(K key); /// EXISTS command (check existence) Future exists(K key); /// EXPIRE command (set expire duration) Future expire(K key, Duration duration); /// KEYS command (get keys that match [pattern]) Future> keys(String pattern); /// Returns the remaining time to live of a key (get value) Future ttl(K key); /// GET command (get value) Future get(K key); /// SET command (set value) Future set(K key, V value); /// SET expire duration command (set value) Future setEx(K key, int ttl, V value); /// GETDEL command (get value and delete value) Future getdel(K key); Future append(K key, V value); Future bitCount(K key, {int? start, int? end}); Future bitOp(String operation, K destKey, List keys); Future bitPos(K key, int bit, {int? start, int? end}); Future decr(K key); Future decrBy(K key, int decrement); Future getBit(K key, int offset); Future getSet(K key, V value); Future incr(K key); Future incrBy(K key, int increment); Future incrByFloat(K key, double increment); Future> mGet(List keys); Future mSet(Map keyValues); Future mSetNX(Map keyValues); Future setBit(K key, int offset, int value); Future pSetEx(K key, int ttl, V value); Future setNx(K key, V value); Future setRange(K key, int offset, V value); Future getRange(K key, int start, int end); Future strlen(K key); Future setOption(String option); Future getOption(String option); } /// list operation commands abstract class ListCommands { /// LRANGE command (get range [startIndex] to [endIndex]) Future> lrange(K key, int startIndex, int endIndex); /// RPUSH command (push to right side) Future rpush(K key, List values); /// LPUSH command (push to left side) Future lpush(K key, List values); /// LSET command (set value that placed in [index]) Future lset(K key, int index, V value); } /// transaction operation commands abstract class TransactionCommands { /// MULTI command (start transaction) Future multi(); /// EXEC command (apply transaction) Future exec(); /// DISCARD command (abort transaction) Future discard(); } /// pubsub operation commands abstract class PubSubCommands { /// PSUBSCRIBE command (pattern subscribe) Stream psubscribe(String pattern); /// PUBLISH command (publish [message] to [channel]) Future publish(String channel, V message); } ================================================ FILE: lib/src/redis/exception.dart ================================================ /// Redis error exception class /// All exceptions thrown from this package inherit from this class. class RedisException implements Exception { final String message; const RedisException(this.message); @override String toString() => 'RedisException: $message'; } /// Convert error exception class class RedisConvertException extends RedisException { const RedisConvertException(String message) : super('convert error: $message'); } ================================================ FILE: lib/src/redis/lowlevel/protocol_client.dart ================================================ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'package:vania/src/redis/exception.dart'; import 'package:vania/src/redis/lowlevel/resp.dart'; /// low level Redis Client class RedisProtocolClient { final Socket _socket; final Queue> _waitingCompleter = ListQueue>(); final Stream> _stream; RedisProtocolClient._(this._socket) : _stream = _socket.asBroadcastStream() { _stream.listen(_onData); } /// create [RedisProtocolClient]'s instance static Future createConnection({ required String host, required int port, }) async { final sock = await Socket.connect(host, port) ..setOption(SocketOption.tcpNoDelay, true); return RedisProtocolClient._(sock); } /// event handling on data received void _onData(List data) { final str = utf8.decode(data); if (_waitingCompleter.isEmpty) { return; } final f = _waitingCompleter.removeFirst(); final resp = Resp.deserialize(str); if (resp == null) { f.completeError(RedisConvertException('failed to convert')); return; } f.complete(resp); } /// send Redis command data. /// [resp]: redis command void sendCommand(Resp resp) { _socket.add(utf8.encode(resp.serialize())); } /// receive command Future receive() { final c = Completer(); _waitingCompleter.addLast(c); return c.future; } /// received data stream Stream get stream => _stream .map((event) => utf8.decode(event)) .map((event) => Resp.deserialize(event)); /// close connection Future close() => _socket.close(); } ================================================ FILE: lib/src/redis/lowlevel/resp.dart ================================================ // ignore_for_file: constant_identifier_names import 'package:vania/src/redis/exception.dart'; /// Redis error reply class RedisError { final String prefix; final String message; RedisError({required this.prefix, required this.message}); @override String toString() => '$prefix: $message'; } /// Redis protocol type enum RespType { STRING, ARRAY, INTEGER, DOUBLE, ERROR, NULL, UNKNOWN } /// Redis protocol data class Resp { static const _CRLF = '\u000d\u000a'; final dynamic value; Resp(this.value); /// serialize value implementation String _serializeValue(dynamic value, {bool isBulkString = true}) { if (value is String) { if (!isBulkString && !value.contains(RegExp('s'))) { return '+$value$_CRLF'; } return '\$${value.length}$_CRLF$value$_CRLF'; } if (value is int) { return ':$value$_CRLF'; } if (value is List) { final data = [ '*${value.length}$_CRLF', ...value.map((e) => _serializeValue(e, isBulkString: true)), ].join(''); return data; } return ''; } /// serialize value in this instance String serialize() => _serializeValue(value); /// deserialize and create [Resp]'s instance. /// [s]: serialized data static Resp? deserialize(String s) => _deserializeEntry(s.split(_CRLF), 0)?.resp; /// type of [value] RespType get type { if (value == null) { return RespType.NULL; } if (value is String) { return RespType.STRING; } if (value is List) { return RespType.ARRAY; } if (value is int) { return RespType.INTEGER; } if (value is double) { return RespType.DOUBLE; } if (value is RedisError) { return RespType.ERROR; } return RespType.UNKNOWN; } /// is [type] == [RespType.NULL] bool get isNull => type == RespType.NULL; /// is [type] == [RespType.STRING] bool get isString => type == RespType.STRING; /// is [type] == [RespType.ARRAY] bool get isList => type == RespType.ARRAY; /// is [type] == [RespType.INTEGER] bool get isInteger => type == RespType.INTEGER; /// is [type] == [RespType.DOUBLE] bool get isDouble => type == RespType.DOUBLE; /// is [type] == [RespType.ERROR] bool get isError => type == RespType.ERROR; /// Get String value if [isString] == true String? get stringValue => isString ? value as String : null; /// Get List value if [isList] == true List? get arrayValue => isList ? value as List : null; /// Get int value if [isInteger] == true int? get integerValue => isInteger ? value as int : null; double? get doubleValue => isInteger ? value as double : null; /// Get [RedisError] value if [isError] == true RedisError? get errorValue => isError ? value as RedisError : null; /// throw exception if [isError] == true void throwIfError() { final err = errorValue; if (err == null) { return; } throw RedisException('$err'); } /// for debug @override String toString() => '$type $stringValue $arrayValue $integerValue $doubleValue $errorValue'; } class _DeserializeResult { final int endIndex; final dynamic value; _DeserializeResult(this.endIndex, this.value); Resp get resp => Resp(value); } extension _SafeAt on List { String? safeAt(int index) => index < length ? this[index] : null; } extension _ToInt on String { int? toInt() => int.tryParse(this); } _DeserializeResult? _deserializeEntry(List s, int startIndex) { final current = s.safeAt(startIndex); if (current == null) { return null; } switch (current[0]) { case '+': // simple strings return _deserializeSimpleString(s, startIndex); case '-': // errors return _deserializeError(s, startIndex); case ':': // integers return _deserializeInteger(s, startIndex); case '\$': // bulk strings return _deserializeBulkString(s, startIndex); case '*': // arrays return _deserializeArray(s, startIndex); } return null; } _DeserializeResult? _deserializeSimpleString(List s, int startIndex) { final value = s.safeAt(startIndex)?.substring(1); if (value == null) { return null; } return _DeserializeResult(startIndex + 1, value); } _DeserializeResult? _deserializeBulkString(List s, int startIndex) { final lengthStr = s.safeAt(startIndex); if (lengthStr == null) { return null; } final length = lengthStr.substring(1).toInt(); if (length == null) { return null; } if (length == -1) { return _DeserializeResult(startIndex + 1, null); } if (length < 0) { return null; } final value = s.sublist(startIndex + 1).join(Resp._CRLF).substring(0, length); return _DeserializeResult( startIndex + value.split(Resp._CRLF).length + 1, value, ); } _DeserializeResult? _deserializeError(List s, int startIndex) { final value = s.safeAt(startIndex)?.substring(1); if (value == null) { return null; } final spl = value.split(' '); final prefix = spl[0]; final message = spl.sublist(1).join(' '); return _DeserializeResult( startIndex + 1, RedisError(prefix: prefix, message: message), ); } _DeserializeResult? _deserializeInteger(List s, int startIndex) { final value = s.safeAt(startIndex)?.substring(1).toInt(); if (value == null) { return null; } return _DeserializeResult(startIndex + 1, value); } _DeserializeResult? _deserializeArray(List s, int startIndex) { final lengthStr = s.safeAt(startIndex); if (lengthStr == null) { return null; } final length = lengthStr.substring(1).toInt(); if (length == null) { return null; } if (length == -1) { return _DeserializeResult(startIndex + 1, null); } if (length < 0) { return null; } final list = []; var index = startIndex + 1; for (var i = 0; i < length; ++i) { final res = _deserializeEntry(s, index); if (res == null) { return null; } list.add(res.value); index = res.endIndex; } return _DeserializeResult(index, list); } ================================================ FILE: lib/src/redis/redis.dart ================================================ import 'dart:async'; import 'package:vania/src/redis/vania_redis.dart'; import 'package:vania/src/utils/helper.dart' show env; class Redis { late Commands command; static final Completer _completer = Completer(); Future get initialized => _completer.future; Redis._internal(); static Redis? _singleton; factory Redis() { if (_singleton == null) { _singleton = Redis._internal(); _singleton!._initRedis().then((_) { _completer.complete(); }); } return _singleton!; } Future _initRedis() async { RedisClient cli = await RedisClient.connect( env('REDIS_HOST', '127.0.0.1'), env('REDIS_PORT', 6379), db: env('REDIS_DB', 0), username: env('REDIS_USERNAME'), password: env('REDIS_PASSWORD'), ); command = cli.getCommands(); } } ================================================ FILE: lib/src/redis/vania_redis.dart ================================================ export 'command/client.dart'; export 'command/codec.dart'; export 'command/commands.dart'; export 'redis.dart'; ================================================ FILE: lib/src/route/middleware/csrf_middleware.dart ================================================ import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:vania/http/response.dart'; import 'package:vania/src/config/config.dart'; import 'package:vania/src/exception/page_expired_exception.dart'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/http/session/session_manager.dart'; import 'package:vania/src/ioc_container.dart'; import 'package:vania/src/utils/functions.dart'; import 'dart:async'; class CsrfMiddleware extends Middleware { final SessionManager _sessionManager = IoCContainer() .resolve(); @override Future handle(Request req) async { if (req.method?.toLowerCase() == 'post' || req.method?.toLowerCase() == 'put' || req.method?.toLowerCase() == 'patch') { List csrfExcept = ['api/*']; csrfExcept.addAll(Config().get('csrf_except') ?? []); String uri = Uri.parse( sanitizeRoutePath(req.uri.toString()), ).path.toLowerCase(); if (!_isUrlExcluded(uri, csrfExcept)) { String requestCookie = req.cookie('XSRF-TOKEN') ?? ''; Map cookie = {}; if (requestCookie.isNotEmpty) { cookie = jsonDecode( utf8.decode(base64.decode(_fixBase64Padding(requestCookie))), ); } String? token = req.input('_csrf') ?? req.input('_token') ?? req.header('X-CSRF-TOKEN'); if (token == null || token.isEmpty) { if (req.isJson()) { throw PageExpiredException( message: 'Security Error: The CSRF token is missing or incorrect', responseType: ResponseType.json, ); } throw PageExpiredException(); } final storedToken = await _sessionManager.getSession( 'x_csrf_token', ); if (storedToken == null || storedToken.isEmpty) { if (req.isJson()) { throw PageExpiredException( message: 'Security Error: The CSRF token is missing or incorrect', responseType: ResponseType.json, ); } throw PageExpiredException(); } if (storedToken != token) { if (req.isJson()) { throw PageExpiredException( message: 'Security Error: The CSRF token is missing or incorrect', responseType: ResponseType.json, ); } throw PageExpiredException(); } String iv = await _sessionManager.getSession('x_csrf_iv'); String expectedCookie = _computeCsrfCookieValue(storedToken, iv); if (expectedCookie != cookie['token']) { if (req.isJson()) { throw PageExpiredException( message: 'Security Error: The CSRF token is missing or incorrect', responseType: ResponseType.json, ); } throw PageExpiredException(); } } } } String _fixBase64Padding(String value) { while (value.length % 4 != 0) { value += '='; } return value; } bool _isUrlExcluded(String path, List csrfExcept) { final cleanPath = path.startsWith('/') ? path.substring(1) : path; for (var pattern in csrfExcept) { final cleanPattern = pattern.startsWith('/') ? pattern.substring(1) : pattern; if (cleanPattern.contains('*')) { final regexStr = cleanPattern .replaceAll('*', '.*') .replaceAll('/', '\\/'); final regex = RegExp('^$regexStr\$', caseSensitive: false); if (regex.hasMatch(cleanPath)) { return true; } } else if (cleanPath.toLowerCase().startsWith( cleanPattern.toLowerCase(), )) { return true; } } return false; } String _computeCsrfCookieValue(String token, String iv) { var hmac = Hmac(sha512, utf8.encode(iv)); final Digest digest = hmac.convert(utf8.encode(token)); return base64.encode(digest.bytes); } } ================================================ FILE: lib/src/route/middleware/throttle.dart ================================================ import 'dart:io'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/http/request/request.dart'; import 'package:vania/src/utils/helper.dart' show env; import '../../exception/throttle_exception.dart'; import '../throttle_requests.dart'; class Throttle extends Middleware { final int maxAttempts; final Duration duration; final bool includeUserIdentifier; final String? customMessage; final Map? headers; final bool bypassInDevelopment; late final ThrottleRequests _throttle; Throttle({ this.maxAttempts = 60, this.duration = const Duration(minutes: 1), this.includeUserIdentifier = false, this.customMessage, this.headers, this.bypassInDevelopment = true, }) { _throttle = ThrottleRequests(maxAttempts: maxAttempts, duration: duration); } @override Future handle(Request req) async { if (bypassInDevelopment && env('APP_ENV', 'development') == 'development') { return; } final String identifier = await _getRequestIdentifier(req); final remaining = _throttle.remainingAttempts(identifier); _addRateLimitHeaders(req.response, remaining); if (!_throttle.request(identifier)) { final retryAfter = _throttle.retryAfter(identifier); throw ThrottleException( message: customMessage ?? 'Too Many Requests. Please try again later.', code: HttpStatus.tooManyRequests, headers: {'Retry-After': retryAfter.inSeconds.toString(), ...?headers}, ); } } Future _getRequestIdentifier(Request req) async { final List parts = [req.ip ?? 'unknown']; if (includeUserIdentifier) { final userMap = req.user; if (userMap != null && userMap['id'] != null) { parts.add(userMap['id'].toString()); } } return parts.join(':'); } void _addRateLimitHeaders(HttpResponse response, int remaining) { response.headers.add('X-RateLimit-Limit', maxAttempts.toString()); response.headers.add('X-RateLimit-Remaining', remaining.toString()); response.headers.add( 'X-RateLimit-Reset', _throttle.resetTime().toIso8601String(), ); } } ================================================ FILE: lib/src/route/route.dart ================================================ import 'package:meta/meta.dart'; import 'router.dart'; class Route { String? get prefix => null; @mustBeOverridden @mustCallSuper void register() { Router.basePrefix(prefix); } } ================================================ FILE: lib/src/route/route_data.dart ================================================ import 'package:vania/src/http/middleware/middleware.dart'; class RouteData { final String method; String path; final Function action; Map? params; List preMiddleware; String? domain; final bool? corsEnabled; final bool hasRequest; final String? prefix; String? name; Map? paramTypes; Map? regex; RouteData({ required this.method, required this.path, required this.action, this.corsEnabled, this.params, this.preMiddleware = const [], this.domain, this.prefix, this.hasRequest = false, this.paramTypes, this.name, this.regex, }); @override String toString() { return 'RouteData{method: $method, path: $path, name: $name}'; } } ================================================ FILE: lib/src/route/route_handler.dart ================================================ import 'dart:io'; import 'package:vania/src/enum/http_request_method.dart'; import 'package:vania/src/exception/not_found_exception.dart'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/route/route_data.dart'; import 'package:vania/src/route/router.dart'; import 'package:vania/src/route/set_static_path.dart'; import 'package:vania/src/utils/functions.dart'; import 'package:vania/src/utils/helper.dart' show env; final Map _regexCache = {}; final _lookupCache = _LruCache<_LookupKey, RouteData?>( env("ROUTE_LOOK_UP_SIZE", 2000), ); final Map> _staticRoutes = {}; final List _dynamicRoutes = []; class _LookupKey { final String method; final String path; final String domain; _LookupKey(this.method, this.path, this.domain); @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other is! _LookupKey) return false; return method == other.method && path == other.path && domain == other.domain; } @override int get hashCode => Object.hash(method, path, domain); } class _LruCache { final int maxSize; final _map = {}; _LruCache(this.maxSize); V? get(K key) { if (!_map.containsKey(key)) return null; final value = _map.remove(key) as V; _map[key] = value; return value; } void put(K key, V value) { if (_map.containsKey(key)) { _map.remove(key); } _map[key] = value; if (_map.length > maxSize) { _map.remove(_map.keys.first); } } void clear() => _map.clear(); bool contains(K key) => _map.containsKey(key); } void initializeRoutes() { final router = Router(); for (var route in router.routes) { final method = route.method.toLowerCase(); final normalizedPath = _normalizePath( _normalizePrefix(route.prefix) + route.path, ); route.regex?.forEach((param, pattern) { _regexCache.putIfAbsent(pattern, () => RegExp(pattern)); }); if (!normalizedPath.contains('{')) { _staticRoutes.putIfAbsent(method, () => []).add(route); } else { _dynamicRoutes.add(route); } } } /// Main HTTP request handler using LRU cache RouteData? httpRouteHandler(HttpRequest req) { final method = req.method.toLowerCase(); final rawPath = sanitizeRoutePath(req.uri.toString()); final requestPath = Uri.decodeComponent(Uri.parse(rawPath).path); final domain = req.headers.value(HttpHeaders.hostHeader) ?? ''; if (setStaticPath(req)) { return null; } final key = _LookupKey(method, requestPath, domain); final cached = _lookupCache.get(key); if (cached != null) { return cached; } final matchedRoute = _findMatchingRoute(requestPath, method, domain); _lookupCache.put(key, matchedRoute); if (matchedRoute == null) { return _handleNotFound(req, method); } return matchedRoute; } RouteData? _handleNotFound(HttpRequest req, String method) { if (method == HttpRequestMethod.options.name.toLowerCase()) { req.response ..headers.add('Content-Length', 0) ..close(); return null; } throw NotFoundException( message: {'message': 'Not found'}, responseType: ResponseType.json, ); } RouteData? _findMatchingRoute( String requestPath, String method, String domain, ) { final staticList = _staticRoutes[method] ?? []; for (final route in staticList) { String fullPath = _normalizePath( _normalizePrefix(route.prefix) + route.path, ); if (fullPath.endsWith('/')) { fullPath = fullPath.substring(0, fullPath.length - 1); } if (fullPath == _normalizePath(requestPath) && _domainMatches(domain, route.domain)) { return _applyDomainParams(route, domain); } } for (final route in _dynamicRoutes.where( (r) => r.method.toLowerCase() == method, )) { if (!_domainMatches(domain, route.domain)) continue; final result = _matchDynamic(requestPath, route, domain); if (result != null) return result; } return null; } bool _domainMatches(String requestDomain, String? routeDomain) { if (routeDomain == null) return true; final pattern = routeDomain.toLowerCase(); if (!pattern.contains('{')) { return requestDomain.toLowerCase() == pattern; } final placeholder = RegExp(r'{([^}]+)}').firstMatch(pattern)!.group(1)!; final actual = requestDomain.split('.').first.toLowerCase(); final expected = pattern.replaceAll('{$placeholder}', actual); return expected == requestDomain.toLowerCase(); } RouteData _applyDomainParams(RouteData route, String domain) { final copy = RouteData( method: route.method, path: route.path, action: route.action, corsEnabled: route.corsEnabled, params: Map.from(route.params ?? {}), preMiddleware: List.from(route.preMiddleware), domain: route.domain, prefix: route.prefix, hasRequest: route.hasRequest, paramTypes: route.paramTypes != null ? Map.from(route.paramTypes!) : null, name: route.name, regex: route.regex != null ? Map.from(route.regex!) : null, ); if (route.domain != null && route.domain!.contains('{')) { final placeholder = RegExp( r'{([^}]+)}', ).firstMatch(route.domain!)!.group(1)!; copy.params![placeholder] = domain.split('.').first; } return copy; } /// Matches dynamic (parameterized) routes and validates params RouteData? _matchDynamic(String requestPath, RouteData route, String domain) { final patternPath = _normalizePath( _normalizePrefix(route.prefix) + route.path, ); final reqParts = _normalizePath(requestPath).split('/'); final patternParts = patternPath.split('/'); if (reqParts.length != patternParts.length) return null; final params = {}; for (var i = 0; i < patternParts.length; i++) { final partPattern = patternParts[i]; final partValue = reqParts[i]; if (partPattern.startsWith('{') && partPattern.endsWith('}')) { final name = partPattern.substring(1, partPattern.length - 1); params[name] = partValue; } else if (partPattern != partValue) { return null; } } if (!_validateParams(params, route)) return null; final routed = _applyDomainParams(route, domain); routed.params = params; return routed; } /// Validates parameter types and regex constraints bool _validateParams(Map params, RouteData route) { if (route.paramTypes != null) { for (final entry in route.paramTypes!.entries) { final name = entry.key; final type = entry.value; if (!params.containsKey(name)) return false; if (type == int) { final val = int.tryParse(params[name].toString()); if (val == null) return false; params[name] = val; } } } if (route.regex != null) { for (final entry in route.regex!.entries) { final name = entry.key; final pattern = entry.value; final regex = _regexCache[pattern]!; if (!regex.hasMatch(params[name].toString())) return false; } } return true; } void clearRouteCaches() { _regexCache.clear(); _lookupCache.clear(); _staticRoutes.clear(); _dynamicRoutes.clear(); } /// Normalizes a route or request path: trims and removes extra slashes String _normalizePath(String path) { return path .trim() .replaceAll('//', '/') .replaceAll(RegExp(r'/+\$'), '') .replaceAll(RegExp(r'^/'), ''); } /// Normalizes the route prefix by ensuring leading slash and no trailing slash String _normalizePrefix(String? prefix) { if (prefix == null || prefix.isEmpty) return ''; final p = prefix.trim(); return '/${p.replaceAll(RegExp(r'^/|/\$'), '')}'; } ================================================ FILE: lib/src/route/route_history.dart ================================================ import 'dart:io'; class RouteHistory { static final RouteHistory _instance = RouteHistory._internal(); factory RouteHistory() => _instance; RouteHistory._internal(); String _currentRoute = ''; String _previousRoute = ''; String get currentRoute => _currentRoute; String get previousRoute => _previousRoute; Future updateRouteHistory(HttpRequest req) async { // Only track HTML responses if (_isHtmlRequest(req)) { _updateRoutes(req.uri.path); } } bool _isHtmlRequest(HttpRequest req) { final acceptHeader = req.headers.value('accept'); return acceptHeader != null && acceptHeader.toString().contains('html'); } void _updateRoutes(String path) { if (_currentRoute.isEmpty) { _currentRoute = path; } else { _previousRoute = _currentRoute; _currentRoute = path; } } } ================================================ FILE: lib/src/route/router.dart ================================================ import 'package:vania/src/enum/http_request_method.dart'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/route/route_data.dart'; import 'package:vania/src/websocket/web_socket_handler.dart'; import 'package:vania/src/websocket/websocket_event.dart'; import '../../vania.dart' show env; class Router { static final Router _singleton = Router._internal(); factory Router() => _singleton; Router._internal(); String? _prefix; String? _groupPrefix; String? _groupDomain; final List _groupMiddleware = []; final List _routes = []; static final RegExp _requestVarRegex = RegExp(r'Closure: \(([^)]*)\) =>'); static final RegExp _closureStartRegex = RegExp(r'Closure: \('); List get routes => List.unmodifiable(_routes); static String url(String name, [Map? params]) { RouteData routeData = Router()._routes .where((route) => route.name == name) .first; if (params == null) { return '${env('APP_URL')}/${routeData.path}'; } final reg = RegExp(r'\{(\w+)\}'); return routeData.path.replaceAllMapped(reg, (match) { final key = match.group(1)!; if (!params.containsKey(key)) { throw ArgumentError('Missing parameter: $key'); } return '${env('APP_URL')}/${params[key].toString()}'; }); } static void basePrefix([String? prefix]) { if (prefix == null) { Router()._prefix = null; return; } Router()._prefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length - 1) : prefix; } bool _getRequestVar(String input) { if (!_closureStartRegex.hasMatch(input)) return false; final match = _requestVarRegex.firstMatch(input); if (match == null) return false; final params = match.group(1)!; final firstParam = params.split(',').firstOrNull; return firstParam == 'Request'; } Router _addRouteInternal( HttpRequestMethod method, String path, Function action, { Map? paramTypes, Map? regex, }) { final bool hasRequest = _getRequestVar(action.toString()); if (!path.startsWith('/')) { path = '/$path'; } final normalizedPath = _normalizePath(path); _routes.add( RouteData( method: method.name, path: normalizedPath, action: action, prefix: _prefix, paramTypes: paramTypes, regex: regex, hasRequest: hasRequest, ), ); return this; } String _normalizePath(String path) { return path.trim(); } static Router _addRoute( HttpRequestMethod method, String path, Function action, ) { return Router() ._addRouteInternal(method, path, action) .middleware(Router()._groupMiddleware) .domain(Router()._groupDomain) .prefix(Router()._groupPrefix); } Router middleware([List? middleware]) { if (middleware != null && _routes.isNotEmpty) { _routes.last.preMiddleware = [ ..._routes.last.preMiddleware, ...middleware, ]; } return this; } Router prefix([String? prefix]) { if (prefix != null && _routes.isNotEmpty) { final route = _routes.last; final basePath = route.path.startsWith('/') ? route.path.substring(1) : route.path; route.path = prefix.endsWith("/") ? "$prefix$basePath" : "$prefix/$basePath"; } return this; } Router name([String? name]) { if (name != null && _routes.isNotEmpty) { _routes.last.name = name; } return this; } Router domain([String? domain]) { if (domain != null && _routes.isNotEmpty) { _routes.last.domain = domain; } return this; } Router whereInt(String paramName) { if (_routes.isNotEmpty) { _routes.last.paramTypes ??= {}; _routes.last.paramTypes![paramName] = int; } return this; } Router whereString(String paramName) { if (_routes.isNotEmpty) { _routes.last.paramTypes ??= {}; _routes.last.paramTypes![paramName] = String; } return this; } Router whereDouble(String paramName) { if (_routes.isNotEmpty) { _routes.last.paramTypes ??= {}; _routes.last.paramTypes![paramName] = double; } return this; } Router whereBool(String paramName) { if (_routes.isNotEmpty) { _routes.last.paramTypes ??= {}; _routes.last.paramTypes![paramName] = bool; } return this; } Router where(String paramName, String regex) { if (_routes.isNotEmpty) { _routes.last.regex ??= {}; _routes.last.regex![paramName] = regex; } return this; } static Router get(String path, Function action) => _addRoute(HttpRequestMethod.get, path, action); static Router post(String path, Function action) => _addRoute(HttpRequestMethod.post, path, action); static Router put(String path, Function action) => _addRoute(HttpRequestMethod.put, path, action); static Router patch(String path, Function action) => _addRoute(HttpRequestMethod.patch, path, action); static Router delete(String path, Function action) => _addRoute(HttpRequestMethod.delete, path, action); static Router options(String path, Function action) => _addRoute(HttpRequestMethod.options, path, action); static Router purge(String path, Function action) => _addRoute(HttpRequestMethod.purge, path, action); static Router copy(String path, Function action) => _addRoute(HttpRequestMethod.copy, path, action); static Router link(String path, Function action) => _addRoute(HttpRequestMethod.link, path, action); static Router unlink(String path, Function action) => _addRoute(HttpRequestMethod.unlink, path, action); static Router lock(String path, Function action) => _addRoute(HttpRequestMethod.lock, path, action); static Router unlock(String path, Function action) => _addRoute(HttpRequestMethod.unlock, path, action); static Router propfind(String path, Function action) => _addRoute(HttpRequestMethod.propfind, path, action); static Router any(String path, Function action) { final router = Router(); final currentPrefix = router._prefix; for (HttpRequestMethod method in HttpRequestMethod.values) { final routeData = RouteData( method: method.name, path: router._normalizePath(path), action: action, prefix: currentPrefix, hasRequest: router._getRequestVar(action.toString()), preMiddleware: List.from(router._groupMiddleware), domain: router._groupDomain, ); router._routes.add(routeData); } return router; } static void resource( String path, dynamic controller, { String? prefix, List? middleware, String? domain, String regex = r'\d+(.\d+)?', }) { Router.get( path, controller.index, ).middleware(middleware).domain(domain).prefix(prefix); Router.get( "$path/create", controller.create, ).middleware(middleware).domain(domain).prefix(prefix); Router.post( path, controller.store, ).middleware(middleware).domain(domain).prefix(prefix); Router.get( "$path/{id}", controller.show, ).middleware(middleware).domain(domain).prefix(prefix).where('id', regex); Router.get( "$path/{id}/edit", controller.edit, ).middleware(middleware).domain(domain).prefix(prefix).where('id', regex); Router.put( "$path/{id}", controller.update, ).middleware(middleware).domain(domain).prefix(prefix).where('id', regex); Router.delete( "$path/{id}", controller.destroy, ).middleware(middleware).domain(domain).prefix(prefix).where('id', regex); } static void websocket( String path, Function(WebSocketEvent) eventCallback, { List? middleware, }) { final currentPrefix = Router()._prefix; String fullPath = path; if (currentPrefix != null) { fullPath = currentPrefix.endsWith('/') ? "$currentPrefix$path" : "$currentPrefix/$path"; } eventCallback( WebSocketHandler().websocketRoute(fullPath, middleware: middleware), ); } static void group( Function callback, { String? prefix = '', List middleware = const [], String? domain, }) { final router = Router(); final previousDomain = router._groupDomain; final previousPrefix = router._groupPrefix; final previousMiddleware = List.from(router._groupMiddleware); router._groupDomain = domain ?? previousDomain; if (router._groupPrefix != null) { if (prefix != null) { if (!prefix.startsWith('/')) { prefix = '/$prefix'; } router._groupPrefix = _joinPrefixes(router._groupPrefix!, prefix); } } else { if (prefix != null) { if (!prefix.startsWith('/')) { prefix = '/$prefix'; } } router._groupPrefix = prefix; } if (middleware.isNotEmpty) { router._groupMiddleware.addAll(middleware); } callback(); router._groupDomain = previousDomain; router._groupPrefix = previousPrefix; router._groupMiddleware ..clear() ..addAll(previousMiddleware); } static String _joinPrefixes(String basePrefix, String newPrefix) { final base = basePrefix.endsWith('/') ? basePrefix : '$basePrefix/'; final prefix = newPrefix.startsWith('/') ? newPrefix.substring(1) : newPrefix; return '$base$prefix'.replaceAll(RegExp(r'//'), '/'); } } ================================================ FILE: lib/src/route/set_static_path.dart ================================================ import 'dart:io'; import 'package:vania/src/http/response/response.dart'; import 'package:vania/src/utils/functions.dart'; import 'package:path/path.dart' as path; bool setStaticPath(HttpRequest req) { String routePath = Uri.decodeComponent( Uri.parse(sanitizeRoutePath(req.uri.toString())).path.toLowerCase(), ); if (!routePath.endsWith("/") && req.method.toLowerCase() == 'get') { if (req.uri.path == '/') { routePath = 'index.html'; } File file = File(sanitizeRoutePath("public/$routePath")); if (file.existsSync()) { Response response = Response.file( path.basename(file.path), file.readAsBytesSync(), ); response.makeResponse(req.response); return true; } else { return false; } } else { return false; } } ================================================ FILE: lib/src/route/throttle_requests.dart ================================================ class ThrottleRequests { final int maxAttempts; final Duration duration; final Map _requests = {}; ThrottleRequests({required this.maxAttempts, required this.duration}); bool request(String identifier) { _cleanup(); final now = DateTime.now(); final data = _requests[identifier] ?? _ThrottleData(attempts: 0, firstAttempt: now); if (data.attempts >= maxAttempts && now.difference(data.firstAttempt) < duration) { return false; } if (now.difference(data.firstAttempt) >= duration) { data.attempts = 1; data.firstAttempt = now; } else { data.attempts++; } _requests[identifier] = data; return true; } int remainingAttempts(String identifier) { _cleanup(); final data = _requests[identifier]; if (data == null) return maxAttempts; if (DateTime.now().difference(data.firstAttempt) >= duration) { return maxAttempts; } return maxAttempts - data.attempts; } Duration retryAfter(String identifier) { final data = _requests[identifier]; if (data == null) return Duration.zero; final elapsed = DateTime.now().difference(data.firstAttempt); if (elapsed >= duration) return Duration.zero; return duration - elapsed; } DateTime resetTime() { return DateTime.now().add(duration); } void _cleanup() { final now = DateTime.now(); _requests.removeWhere( (_, data) => now.difference(data.firstAttempt) >= duration, ); } } class _ThrottleData { DateTime firstAttempt; int attempts; _ThrottleData({required this.firstAttempt, required this.attempts}); } ================================================ FILE: lib/src/server/base_http_server.dart ================================================ import 'dart:io'; import 'package:args/args.dart'; import 'package:vania/src/http/request/request_handler.dart'; import 'package:vania/src/utils/helper.dart' show env; import '../ioc_container.dart'; import 'initialize_config.dart'; class BaseHttpServer { final Map config; final List args; BaseHttpServer({required this.config, this.args = const []}); HttpServer? httpServer; /// Starts the HTTP server with the current configuration. /// /// If the application is configured to use a secure connection, the server /// will be started using HTTPS with the provided certificate and private key. /// Otherwise, it will start an HTTP server. /// /// The server listens for incoming HTTP requests using the `httpRequestHandler`. /// /// If the `APP_DEBUG` environment variable is set to true, the server's URL /// will be printed to the console. /// /// An optional [onError] callback can be provided to handle server start errors. /// /// Returns a [Future] that completes with the started [HttpServer] instance. /// /// Throws an error if the server fails to start. Future startServer({Function? onError}) async { String host = env('APP_HOST', InternetAddress.anyIPv6.host); int port = env('APP_PORT', 8000); if (args.isNotEmpty) { final parser = ArgParser() ..addOption('host', abbr: 'h') ..addOption('port', abbr: 'p'); ArgResults results; try { results = parser.parse(args); } on ArgParserException catch (e) { stderr.writeln('Error: ${e.message}\n'); stderr.writeln(parser.usage); exit(64); } if (results['host'] != null) { host = results['host']; } if (results['port'] != null) { port = int.tryParse(results['port']) ?? 8000; } } try { await initializeConfig(config); if (env('APP_SECURE', false)) { String certificateChain = env('APP_CERTIFICATE'); String serverKey = env('APP_PRIVATE_KEY'); String password = env('APP_PRIVATE_KEY_PASSWORD'); SecurityContext context = SecurityContext() ..useCertificateChain(certificateChain) ..usePrivateKey(serverKey, password: password); httpServer = await HttpServer.bindSecure( host, port, context, shared: env('APP_SHARED', false), ); } else { httpServer = await HttpServer.bind( host, port, shared: env('APP_SHARED', false), ); } httpServer?.listen(IoCContainer().resolve().handle); if (env('APP_DEBUG')) { stderr.writeln('Server started on http://127.0.0.1:$port'); } return httpServer!; } catch (e) { stderr.writeln('${DateTime.now().toUtc()} ERROR: $e'); rethrow; } } } ================================================ FILE: lib/src/server/initialize_config.dart ================================================ import 'package:vania/src/route/route_handler.dart'; import '../database/_database_utils/_db_config.dart'; import '../config/config.dart'; import '../database/_connection_manager.dart'; import '../service/service_provider.dart'; import '../utils/helper.dart'; Future initializeConfig(Map config) async { Config().setApplicationConfig = config; if (env('DB_CONNECTION') != null && config['database'] != null) { final Map database = config['database']; ConnectionManager().defaultConnection = database['default']; Map connections = database['connections']; await ConnectionManager().connect( _config(connections[ConnectionManager().defaultConnection]), database['default'], ); // Handle additional connections List additionalConnections = database['additional_connections'] ?? []; if (additionalConnections.isNotEmpty) { for (String connection in additionalConnections) { await ConnectionManager().connect( _config(connections[connection]), connection, ); } } } List providers = config['providers']; for (ServiceProvider provider in providers) { await provider.register(); await provider.boot(); } initializeRoutes(); } DBConfig _config(Map database) => DBConfig( driver: database['driver'] ?? '', host: database['host'] ?? '', port: database['port'] ?? '', database: database['database'] ?? '', username: database['username'] ?? '', password: database['password'] ?? '', sslMode: database['sslmode'] ?? '', collation: database['collation'] ?? 'utf8', timezone: database['timezone'] ?? 'UTC', pool: database['pool'], poolSize: database['poolsize'], filePath: database['file_path'] ?? '', schema: database['schema'] ?? 'public', openInMemorySQLite: database['openInMemorySQLite'] ?? false, ); ================================================ FILE: lib/src/service/service_provider.dart ================================================ abstract class ServiceProvider { const ServiceProvider(); Future boot(); Future register(); } ================================================ FILE: lib/src/storage/local_storage.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as path; import '../utils/helper.dart'; import 'storage_driver.dart'; class LocalStorage implements StorageDriver { static final LocalStorage _instance = LocalStorage._internal(); factory LocalStorage() => _instance; LocalStorage._internal(); final String _storageDir = storagePath('app/public'); @override Future delete(String file) async { final targetFile = File(path.join(_storageDir, file)); if (!await targetFile.exists()) return false; try { await targetFile.delete(); return true; } catch (e) { return false; } } @override Future exists(String file) async { return await File(path.join(_storageDir, file)).exists(); } @override Future getAsBytes(String file) async { final targetFile = File(path.join(_storageDir, file)); if (!await targetFile.exists()) return null; try { return targetFile.readAsBytes(); } catch (e) { return null; } } @override Future get(String file) async { final targetFile = File(path.join(_storageDir, file)); if (!await targetFile.exists()) return null; try { return targetFile.readAsString(); } catch (e) { return null; } } @override Future?> json(String file) async { final content = await get(file); if (content == null) return null; try { return jsonDecode(content) as Map; } catch (e) { return null; } } @override Future put(String path, dynamic content) async { final targetFile = File(_getFullPath(path)); await _ensureDirectoryExists(targetFile.parent); try { if (content is List) { await targetFile.writeAsBytes(content); } else { await targetFile.writeAsString(content.toString()); } return path; } catch (e) { throw Exception('Failed to write file: $e'); } } @override Future mimeType(String file) async { final targetFile = File(path.join(_storageDir, file)); if (!await targetFile.exists()) return null; try { final bytes = await targetFile.readAsBytes(); return lookupMimeType(file, headerBytes: bytes); } catch (e) { return null; } } @override Future size(String file) async { final targetFile = File(path.join(_storageDir, file)); if (!await targetFile.exists()) return null; try { return targetFile.length(); } catch (e) { return null; } } String _getFullPath(String filePath) { return path.join(_storageDir, filePath); } Future _ensureDirectoryExists(Directory directory) async { if (!await directory.exists()) { await directory.create(recursive: true); } } @override String fullPath(String file) => path.join(_storageDir, file); } ================================================ FILE: lib/src/storage/s3_storage.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:mime/mime.dart'; import 'package:vania/src/aws/s3_client.dart'; import 'storage_driver.dart'; class S3Storage implements StorageDriver { static final S3Storage _instance = S3Storage._internal(); factory S3Storage() => _instance; S3Storage._internal(); final S3Client _s3Client = S3Client(); final Map _metadataCache = {}; static const Duration _metadataCacheDuration = Duration(minutes: 5); String removeLeadingSlash(String file) { return file.startsWith('/') ? file.replaceFirst('/', '') : file; } @override String fullPath(String file) { return _s3Client.buildUri(file).toString(); } @override Future put(String filePath, dynamic content) async { filePath = removeLeadingSlash(filePath); final uri = _s3Client.buildUri(filePath); final client = HttpClient(); try { final request = await client.putUrl(uri); final contentType = lookupMimeType(filePath) ?? 'application/octet-stream'; request.headers.set('Content-Type', contentType); request.headers.set('Content-Length', content.length.toString()); final payloadHash = sha256.convert(content).toString(); _s3Client .generateS3Headers('PUT', filePath, hash: payloadHash) .forEach((key, value) => request.headers.set(key, value)); request.add(content); final response = await request.close(); if (response.statusCode == 200) { _invalidateMetadataCache(filePath); return uri.toString(); } throw Exception('Failed to upload file: ${response.statusCode}'); } finally { client.close(); } } @override Future get(String file) async { file = removeLeadingSlash(file); final client = HttpClient(); try { final response = await _executeRequest(client, 'GET', file); if (response.statusCode == 200) { return await response.transform(utf8.decoder).join(); } return null; } finally { client.close(); } } @override Future getAsBytes(String file) async { file = removeLeadingSlash(file); final client = HttpClient(); try { final response = await _executeRequest(client, 'GET', file); if (response.statusCode == 200) { return await response .fold(BytesBuilder(), (b, d) => b..add(d)) .then((b) => b.takeBytes()); } return null; } finally { client.close(); } } @override Future?> json(String file) async { final content = await get(removeLeadingSlash(file)); if (content == null) return null; try { return jsonDecode(content) as Map; } catch (e) { return null; } } @override Future mimeType(String file) async { final metadata = await _getMetadata(file); if (metadata?.contentType != null) { return metadata!.contentType; } final bytes = await getAsBytes(file); if (bytes != null) { return lookupMimeType( file, headerBytes: bytes.sublist(0, min(4096, bytes.length)), ); } return null; } @override Future size(String file) async { final metadata = await _getMetadata(file); return metadata?.contentLength; } @override Future exists(String file) async { final metadata = await _getMetadata(file); return metadata != null; } @override Future delete(String file) async { file = removeLeadingSlash(file); final client = HttpClient(); try { final response = await _executeRequest(client, 'DELETE', file); final success = response.statusCode == 204; if (success) { _invalidateMetadataCache(file); } return success; } finally { client.close(); } } Future _executeRequest( HttpClient client, String method, String file, ) async { final uri = _s3Client.buildUri(file); final request = await _getRequestForMethod(client, method, uri); _s3Client .generateS3Headers(method, file) .forEach((key, value) => request.headers.set(key, value)); return await request.close(); } Future _getRequestForMethod( HttpClient client, String method, Uri uri, ) async { switch (method) { case 'GET': return await client.getUrl(uri); case 'PUT': return await client.putUrl(uri); case 'DELETE': return await client.deleteUrl(uri); case 'HEAD': return await client.headUrl(uri); default: throw UnsupportedError('Unsupported HTTP method: $method'); } } Future<_CachedMetadata?> _getMetadata(String file) async { file = removeLeadingSlash(file); // Check cache first final cached = _metadataCache[file]; if (cached != null && !cached.isExpired) { return cached; } final client = HttpClient(); try { final response = await _executeRequest(client, 'HEAD', file); if (response.statusCode != 200) return null; final metadata = _CachedMetadata( contentLength: int.tryParse( response.headers.value('content-length') ?? '', ), contentType: response.headers.value('content-type'), lastModified: response.headers.value('last-modified'), ); _metadataCache[file] = metadata; return metadata; } finally { client.close(); } } void _invalidateMetadataCache(String file) { _metadataCache.remove(file); } } class _CachedMetadata { final num? contentLength; final String? contentType; final String? lastModified; final DateTime cacheTime; _CachedMetadata({this.contentLength, this.contentType, this.lastModified}) : cacheTime = DateTime.now(); bool get isExpired => DateTime.now().difference(cacheTime) > S3Storage._metadataCacheDuration; } ================================================ FILE: lib/src/storage/storage.dart ================================================ import 'dart:typed_data'; import 'package:vania/src/storage/local_storage.dart'; import 'package:vania/src/storage/s3_storage.dart'; import 'storage_driver.dart'; import 'package:vania/src/utils/helper.dart' show env; import 'package:path/path.dart' as path; class Storage { static final Storage _singleton = Storage._internal(); factory Storage() => _singleton; Storage._internal(); final StorageDriver _driver = switch (env('STORAGE', 'local')) { 'local' => LocalStorage(), 's3' => S3Storage(), _ => LocalStorage(), }; static Future delete(String file) async { return await Storage()._driver.delete(file); } static Future exists(String file) async { return await Storage()._driver.exists(file); } static Future getAsBytes(String file) async { return await Storage()._driver.getAsBytes(file); } static Future get(String file) async { return await Storage()._driver.get(file); } static Future?> json(String file) async { return await Storage()._driver.json(file); } static Future put( String directory, String file, dynamic content, ) async { if (content == null) { throw Exception("Content can't be null"); } String fullPath = path.join(directory, file); if (content is List) { return Storage()._driver.put(fullPath, content); } else if (content is String) { return Storage()._driver.put(fullPath, content); } else if (content is Stream>) { final data = await content.fold>([], (previous, element) { previous.addAll(element); return previous; }); return Storage()._driver.put(fullPath, data); } else { throw Exception( 'Content must be a list of int, a string, or a Stream>.', ); } } static Future mimeType(String file) async { return await Storage()._driver.mimeType(file); } static Future size(String file) async { return Storage()._driver.size(file); } } ================================================ FILE: lib/src/storage/storage_driver.dart ================================================ import 'dart:typed_data'; abstract class StorageDriver { Future put(String filename, dynamic content); Future get(String filename); Future getAsBytes(String filename); Future?> json(String filename); Future mimeType(String filename); Future size(String filename); String fullPath(String file); Future exists(String filename); Future delete(String filename); } ================================================ FILE: lib/src/utils/_pluralize.dart ================================================ class Pluralize { static Pluralize? _singleton; factory Pluralize() { _singleton ??= Pluralize._internal(); return _singleton!; } Pluralize._internal(); final Map irregulars = { 'child': 'children', 'goose': 'geese', 'man': 'men', 'woman': 'women', 'tooth': 'teeth', 'foot': 'feet', 'mouse': 'mice', 'person': 'people', 'ox': 'oxen', 'deer': 'deer', 'sheep': 'sheep', 'fish': 'fish', 'series': 'series', 'species': 'species', 'aircraft': 'aircraft', 'cactus': 'cacti', 'analysis': 'analyses', 'diagnosis': 'diagnoses', 'ellipsis': 'ellipses', 'phenomenon': 'phenomena', 'criterion': 'criteria', 'datum': 'data', 'index': 'indices', 'matrix': 'matrices', 'vertex': 'vertices', 'axis': 'axes', 'die': 'dice', 'bacterium': 'bacteria', 'memorandum': 'memoranda', 'curriculum': 'curricula', 'formula': 'formulae', 'addendum': 'addenda', 'medium': 'media', 'stratum': 'strata', 'focus': 'foci', 'alumnus': 'alumni', 'genus': 'genera', 'stimulus': 'stimuli', 'automaton': 'automata', 'thesis': 'theses', 'crisis': 'crises', 'appendix': 'appendices', 'barracks': 'barracks', 'headquarters': 'headquarters', }; String make(String singular) { if (singular.isEmpty) return singular; if (singular.endsWith('ies') || singular.endsWith('ses') || singular.endsWith('xes') || singular.endsWith('zes') || singular.endsWith('ches') || singular.endsWith('shes') || singular.endsWith('oes')) { return singular; } if (irregulars.containsKey(singular.toLowerCase())) { String plural = irregulars[singular.toLowerCase()]!; if (singular[0].toUpperCase() == singular[0]) { return plural[0].toUpperCase() + plural.substring(1); } return plural; } if (singular.endsWith('y') && singular.length > 1 && !_isVowel(singular[singular.length - 2])) { return '${singular.substring(0, singular.length - 1)}ies'; } if (singular.endsWith('s') || singular.endsWith('x') || singular.endsWith('z') || singular.endsWith('ch') || singular.endsWith('sh') || (singular.endsWith('o') && !_isVowelBeforeO(singular))) { return '${singular}es'; } return '${singular}s'; } static bool _isVowel(String char) { return 'aeiou'.contains(char.toLowerCase()); } static bool _isVowelBeforeO(String word) { if (word.length < 2 || !word.endsWith('o')) return false; return _isVowel(word[word.length - 2]); } String pluralizeVariableName(String variableName) { if (variableName.isEmpty) return variableName; if (variableName.contains('_')) { List parts = variableName.split('_'); parts[parts.length - 1] = make(parts.last); return parts.join('_'); } RegExp camelCaseRegex = RegExp(r'[A-Z][a-z]*$'); Match? match = camelCaseRegex.firstMatch(variableName); if (match != null && match.start > 0) { String lastWord = match.group(0)!; String pluralLastWord = make(lastWord); return variableName.substring(0, match.start) + pluralLastWord; } else { return make(variableName); } } } ================================================ FILE: lib/src/utils/functions.dart ================================================ import 'dart:math'; String toSnakeCase(String input) { if (input.isEmpty) return input; final result = input.replaceAllMapped( RegExp(r'[A-Z]'), (match) => (match.start > 0 ? '_' : '') + match.group(0)!.toLowerCase(), ); return result.toLowerCase(); } /// Sanitizes a route path by replacing multiple slashes with a single slash and /// removing leading and trailing slashes. String sanitizeRoutePath(String path) { path = path.replaceAll(RegExp(r'/+'), '/'); return path.replaceAll(RegExp('^\\/+|\\/+\$'), ''); } /// Generates a random string of a given [length] with the given character set. /// /// The default length is 32. The default character set is all letters of the /// alphabet, both lowercase and uppercase. If [numbers] or [special] is true, /// the character set is extended to include numbers or special characters, /// respectively. The generated string is a random permutation of the characters /// in the character set. String randomString({ int length = 32, bool numbers = false, bool special = false, }) { List strList = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' .split(''); if (numbers) { strList.addAll('1234567890'.split('')); } if (special) { strList.addAll('!@#%^&*()_'.split('')); } strList.shuffle(); String chars = strList.join(''); Random rnd = Random(); return String.fromCharCodes( Iterable.generate( length, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)), ), ); } /// Generate a random number as a string of a given length /// /// If T is int, it will be parsed as an integer and returned as a int /// otherwise, it will be returned as a string /// /// The generated numbers are all positive /// /// The default length is 6 /// /// [length] is the length of the generated number /// /// Returns a random number as a string of [length] length /// /// Example: /// /// var rand = randomInt(); // '123456' /// var rand = randomInt(3); // '246' /// var rand = randomInt(3); // 246 T randomInt([int length = 6]) { List strList = '1234567890'.split(''); strList.shuffle(); String chars = strList.join(''); Random rnd = Random(); String random = String.fromCharCodes( Iterable.generate( length, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)), ), ); if (T is int) { return int.parse(random) as T; } return random as T; } ================================================ FILE: lib/src/utils/helper.dart ================================================ import 'package:vania/src/authentication/gate/gate.dart'; import 'package:vania/src/env_handler/env.dart'; import 'package:vania/src/exception/http_exception.dart'; import 'package:vania/src/ioc_container.dart'; import 'package:vania/src/localization_handler/localization.dart'; import 'package:vania/src/view_engine/template_engine.dart'; import '../http/session/session_manager.dart'; import '../http/validation/field_validation.dart'; String storagePath(String file) => 'storage/$file'; String publicPath(String file) => 'public/$file'; String url(String path) => '${env('APP_URL')}/$path'; String assets(String src) => url(src); FieldValidation field(String fieldName) => FieldValidation(fieldName); bool can(String ability) => Gate().allows(ability); bool cannot(String ability) => Gate().denies(ability); T env(String key, [dynamic defaultValue]) => Env.get(key, defaultValue); String trans(String key, {Map? args, String? locale}) => Localization().trans(key, args, locale); void setLocale(String locale) => Localization().setLocale(locale); bool isLocale(String locale) => Localization().isLocale(locale); void abort(int code, String message) { throw HttpResponseException(message: message, code: code); } Future setSession(String key, dynamic value) async => await IoCContainer().resolve().setSession(key, value); Future getSession(String key) async => TemplateEngine().sessions[key] ?? await IoCContainer().resolve().getSession(key); Future?> allSessions() async => IoCContainer().resolve().allSessions; Future deleteSession(String key) async => await IoCContainer().resolve().deleteSession(key); Future destroyAllSessions() async => await IoCContainer().resolve().destroyAllSessions(); ================================================ FILE: lib/src/utils/request_helper.dart ================================================ import 'dart:io'; import '../http/request/request_body.dart' show RequestBody; import '../http/request/request_handler.dart' show globalHttpRequest; T? getParam(String key, [dynamic defualtValue]) { dynamic param = globalHttpRequest!.uri.queryParameters[key]; if (param == null && defualtValue == null) { return null; } param ??= defualtValue; if (T.toString() == 'int') { return int.tryParse(param.toString()) as T; } if (T.toString() == 'bool') { return bool.tryParse(param.toString()) as T; } if (T.toString() == 'num') { return num.tryParse(param.toString()) as T; } if (T.toString() == 'double') { return double.tryParse(param.toString()) as T; } return param as T; } Uri get requestUri => globalHttpRequest!.uri; String? get clienIp => globalHttpRequest?.connectionInfo?.remoteAddress.address; HttpHeaders? get requestHeaders => globalHttpRequest?.headers; ContentType? get requestContentType => globalHttpRequest?.headers.contentType; String? get method => globalHttpRequest?.method.toUpperCase(); HttpResponse httpResponse = globalHttpRequest!.response; Future requestBody() async { final whereMethod = ['post', 'patch', 'put', 'delete'] .where((method) => method == globalHttpRequest?.method.toLowerCase()) .toList(); if (whereMethod.isNotEmpty) { return await RequestBody.extractBody(request: globalHttpRequest!); } return {}; } ================================================ FILE: lib/src/view_engine/helper.dart ================================================ import 'package:vania/src/http/response/response.dart'; import 'template_engine.dart'; Response view(String view, [Map? context]) => Response.html(TemplateEngine().render(view, context)); ================================================ FILE: lib/src/view_engine/processor_engine/abs_processor.dart ================================================ /// An interface (abstract class) for replacing placeholders in a template. abstract class AbsProcessor { /// Replaces the placeholders in [content] with data from [context]. String parse(String content, [Map? context]); } ================================================ FILE: lib/src/view_engine/processor_engine/assets_processor.dart ================================================ import 'package:vania/src/utils/helper.dart'; import 'abs_processor.dart'; class AssetsProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final assetsPattern = RegExp( r"(\/?)\{@\s*assets\(\s*'([^']*)'\s*\)\s*@\}", dotAll: true, ); return content.replaceAllMapped(assetsPattern, (match) { final assets = match.group(2); return url(assets ?? ''); }); } } ================================================ FILE: lib/src/view_engine/processor_engine/comment_processor.dart ================================================ import 'abs_processor.dart'; class CommentProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final commentPattern = RegExp(r"\{\@\#.*?\#\@\}", dotAll: true); return content.replaceAllMapped(commentPattern, (_) { return ''; }); } } ================================================ FILE: lib/src/view_engine/processor_engine/csrf_processor.dart ================================================ import 'package:vania/src/http/session/session_manager.dart'; import 'package:vania/src/ioc_container.dart'; import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; class CsrfProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final csrfPattern = RegExp(r"\{@\s*csrf\s*@\}", dotAll: true); return content.replaceAllMapped(csrfPattern, (match) { String csrfToken = IoCContainer().resolve().csrfToken; return ''; }); } } ================================================ FILE: lib/src/view_engine/processor_engine/csrf_token_processor.dart ================================================ import 'package:vania/src/http/session/session_manager.dart'; import 'package:vania/src/ioc_container.dart'; import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; class CsrfTokenProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final csrfMethodPattern = RegExp( r"\{@\s*csrf_token\(\)\s*@\}", dotAll: true, ); return content.replaceAllMapped(csrfMethodPattern, (match) { return IoCContainer().resolve().csrfToken; }); } } ================================================ FILE: lib/src/view_engine/processor_engine/error_processor.dart ================================================ import 'package:vania/src/view_engine/template_engine.dart'; import 'abs_processor.dart'; class ErrorProcessor extends AbsProcessor { @override String parse(String content, [Map? context]) { final hasErrorPattern = RegExp( r"hasError\(\s*'([^']*)'\s*\)", dotAll: true, ); content = content.replaceAllMapped(hasErrorPattern, (match) { final errorKey = match.group(1); return TemplateEngine().sessionErrors.containsKey(errorKey).toString(); }); final errorPattern = RegExp( r"\{@\s*error\(\s*'([^']*)'\s*\)\s*@\}", dotAll: true, ); content = content.replaceAllMapped(errorPattern, (error) { final errorKey = error.group(1); return TemplateEngine().sessionErrors[errorKey] ?? ''; }); return content; } } ================================================ FILE: lib/src/view_engine/processor_engine/evaluate_expression.dart ================================================ bool evaluateExpression(String expression, Map context) { expression = _stripOuterParens(expression.trim()); // Check for comparison: e.g. "leftSide == rightSide" // We separate the string into leftSide, operator, rightSide final comparisonPattern = RegExp(r'(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)'); final compMatch = comparisonPattern.firstMatch(expression); if (compMatch != null) { final leftRaw = compMatch.group(1)!.trim(); final op = compMatch.group(2)!.trim(); final rightRaw = compMatch.group(3)!.trim(); final leftVal = _evalArithmetic(leftRaw, context); final rightVal = _evalArithmetic(rightRaw, context); return _compare(leftVal, rightVal, op); } final singleVal = _evalArithmetic(expression, context); if (singleVal is bool) return singleVal; if (singleVal is num) return singleVal != 0; if (singleVal is String && singleVal.toLowerCase() == 'true') return true; final ctxVal = context[expression]; if (ctxVal is bool) return ctxVal; return false; } /// Removes one set of outer parentheses if they exist, e.g. "(10 % 2)" => "10 % 2" String _stripOuterParens(String expr) { final parenPattern = RegExp(r'^\((.*)\)$'); final match = parenPattern.firstMatch(expr); if (match != null) { return match.group(1)!.trim(); } return expr; } /// Evaluates a single arithmetic expression, e.g. "10 % 2", "i + 1", "5 * 3", or just "i". /// Returns `num`, `bool`, or `String` depending on what we find. dynamic _evalArithmetic(String expr, Map ctx) { expr = expr.trim(); final asInt = int.tryParse(expr); if (asInt != null) return asInt; final asDouble = double.tryParse(expr); if (asDouble != null) return asDouble; final arithmeticPattern = RegExp(r'(.+?)\s*([\+\-\*\/%])\s*(.+)'); final match = arithmeticPattern.firstMatch(expr); if (match != null) { final leftRaw = match.group(1)!.trim(); final op = match.group(2)!.trim(); final rightRaw = match.group(3)!.trim(); final leftVal = _evalArithmetic(leftRaw, ctx); final rightVal = _evalArithmetic(rightRaw, ctx); return _doArithmetic(leftVal, rightVal, op); } final valFromContext = ctx[expr]; if (valFromContext != null) return valFromContext; if (expr.toLowerCase() == 'true') return true; if (expr.toLowerCase() == 'false') return false; return expr; } /// Perform arithmetic on leftVal and rightVal with the single operator (+, -, *, /, %). dynamic _doArithmetic(dynamic left, dynamic right, String op) { if (left is num && right is num) { switch (op) { case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return (right == 0) ? null : left / right; case '%': return (right == 0) ? null : left % right; } } if (op == '+') { return '${left?.toString() ?? ''}${right?.toString() ?? ''}'; } return null; } String removeOuterQuotes(String input) { if ((input.startsWith("'") && input.endsWith("'")) || (input.startsWith('"') && input.endsWith('"'))) { return input.substring(1, input.length - 1); } return input; } /// Compare leftVal and rightVal with the given operator bool _compare(dynamic leftVal, dynamic rightVal, String op) { if (leftVal is num && rightVal is num) { switch (op) { case '==': return leftVal == rightVal; case '!=': return leftVal != rightVal; case '>': return leftVal > rightVal; case '>=': return leftVal >= rightVal; case '<': return leftVal < rightVal; case '<=': return leftVal <= rightVal; } } if (op == '==') { return leftVal?.toString() == removeOuterQuotes(rightVal?.toString() ?? ""); } if (op == '!=') { return leftVal?.toString() != removeOuterQuotes(rightVal?.toString() ?? ""); } if (op == '>' || op == '>=' || op == '<' || op == '<=') { final lstr = leftVal?.toString() ?? ''; final rstr = rightVal?.toString() ?? ''; final cmp = lstr.compareTo(rstr); switch (op) { case '>': return cmp > 0; case '>=': return cmp >= 0; case '<': return cmp < 0; case '<=': return cmp <= 0; } } return false; } ================================================ FILE: lib/src/view_engine/processor_engine/extends_processor.dart ================================================ import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; import 'package:vania/src/view_engine/template_reader.dart'; class ExtendsProcessor implements AbsProcessor { /// Parse `{@ extends('template_path') @}` blocks in [content] and replace them with the content of the parent template. /// /// The parent template path is read from the file system using [FileTemplateReader]. /// /// The following example: /// @override String parse(String content, [Map? context]) { final extendsPattern = RegExp(r"\{@\s*extends\(\s*'([^']+)'\s*\)\s*@\}"); final match = extendsPattern.firstMatch(content); if (match == null) { return content; } final parentLayoutPath = match.group(1); if (parentLayoutPath == null) { return content; } content = content.replaceFirst(extendsPattern, ''); final parentTemplate = FileTemplateReader().read(parentLayoutPath); return parentTemplate; } } ================================================ FILE: lib/src/view_engine/processor_engine/for_loop_processor.dart ================================================ import '../template_engine.dart'; import 'abs_processor.dart'; /// processor to handles "for" loops, including **nested** loops. class ForLoopProcessor extends AbsProcessor { @override String parse(String content, [Map? context]) { context ??= {}; return _parseForLoops(content, context); } /// Parse and replace `{@ for ... @}` blocks in [template] with values from [context]. /// /// This implementation supports nested loops. /// /// The replacement is done by expanding the loop content for each iteration of the loop. /// The loop variable is iterated over the iterable returned by the expression. /// Each iteration is replaced with the value of the loop variable. /// /// The following example: ///```html /// {@ for user in users @} /// {@ user.name @} /// {@ endfor @} /// /// {@ for i=0; i<3; i++ @} /// Index ->: @{i} - Name: @{users[i].name} /// {@ endfor @} ///``` /// Is replaced with the contents of the loop block, with `user` replaced with each value of the iterable returned by the expression. /// String _parseForLoops(String template, Map context) { final buffer = StringBuffer(); int index = 0; while (true) { final startPos = template.indexOf('{@ for', index); if (startPos == -1) { buffer.write(template.substring(index)); break; } buffer.write(template.substring(index, startPos)); final forStartClose = template.indexOf('@}', startPos); if (forStartClose == -1) { buffer.write(template.substring(startPos)); break; } final forExpression = template .substring(startPos + 6, forStartClose) .trim(); int loopContentStart = forStartClose + 2; int searchPos = loopContentStart; int nested = 0; int endforPos = -1; while (true) { final nextFor = template.indexOf('{@ for', searchPos); final nextEndfor = template.indexOf('{@ endfor @}', searchPos); if (nextEndfor == -1) { break; } if (nextFor != -1 && nextFor < nextEndfor) { nested++; searchPos = nextFor + 1; } else { if (nested > 0) { nested--; searchPos = nextEndfor + 1; } else { endforPos = nextEndfor; break; } } } if (endforPos == -1) { buffer.write(template.substring(loopContentStart)); break; } final loopBlock = template.substring(loopContentStart, endforPos); final expanded = _expandLoop(forExpression, loopBlock, context); buffer.write(expanded); final endforClose = endforPos + '{@ endfor @}'.length; index = endforClose; } return buffer.toString(); } /// Expands a loop expression into the rendered output based on the specified loop style. /// /// Handles two types of loop expressions: /// 1. C-style loops (e.g., `i=0; i<10; i++`), where it extracts the loop /// control variables, start condition, end condition, and increment expression. /// 2. "Item in list" style loops (e.g., `item in items`), where it iterates /// over each element in the list and processes the loop block. /// /// If the expression matches a C-style loop, it delegates execution to /// `_runCStyleLoop`. If it matches an "item in list" loop, it delegates to /// `_runItemInListLoop`. Returns the expanded loop content as a string. String _expandLoop( String forExpression, String loopBlock, Map context, ) { final cStylePattern = RegExp( r'^(\w+)\s*=\s*(.+?);\s*\1\s*([<>]=?|[<>])\s*(.+?);\s*(.+)$', ); final cMatch = cStylePattern.firstMatch(forExpression); if (cMatch != null) { final varName = cMatch.group(1)!; final startExpr = cMatch.group(2)!; final operator = cMatch.group(3)!; final endExpr = cMatch.group(4)!; final incExpr = cMatch.group(5)!; return _runCStyleLoop( loopBlock: loopBlock, varName: varName, startExpr: startExpr, operator: operator, endExpr: endExpr, incExpr: incExpr, context: context, ); } final itemInListPattern = RegExp(r'^(\w+)\s+in\s+(\w+)$'); final inMatch = itemInListPattern.firstMatch(forExpression); if (inMatch != null) { final itemName = inMatch.group(1)!; final listName = inMatch.group(2)!; return _runItemInListLoop(loopBlock, itemName, listName, context); } return ''; } /// Runs a loop block for each item in a list. /// /// The loop block is rendered for each item in the list, with the item /// assigned to a variable with the name given by [itemName]. The item's /// index in the list is also available as a variable named `'index'`. /// /// The loop block is rendered by calling [TemplateEngine().renderString] with /// the loop block as the template and a new context that includes the current /// item and index, as well as all of the variables from the original context. /// /// The output of the loop block is concatenated together and returned as a /// single string. If the specified list is not a list, an empty string is /// returned. String _runItemInListLoop( String loopBlock, String itemName, String listName, Map context, ) { final listObj = context[listName]; if (listObj is! List) return ''; final buffer = StringBuffer(); for (var i = 0; i < listObj.length; i++) { final item = listObj[i]; final subCtx = {...context, itemName: item, 'index': i}; buffer.write(TemplateEngine().renderString(loopBlock, subCtx)); } return buffer.toString(); } /// Runs a C-style for loop block for each iteration of the loop. /// /// The loop block is rendered for each iteration of the loop, with the current /// value of the loop variable assigned to a variable with the name given by /// [varName]. The loop block is rendered by calling /// [TemplateEngine().renderString] with the loop block as the template and a /// new context that includes the current value of the loop variable, as well /// as all of the variables from the original context. /// /// The output of the loop block is concatenated together and returned as a /// single string. If the specified loop variables are not valid, an empty /// string is returned. /// /// The loop iterates until the condition specified by [operator] is false. /// The condition is evaluated by calling [TemplateEngine().renderString] with /// the condition expression as the template and the current context. /// String _runCStyleLoop({ required String loopBlock, required String varName, required String startExpr, required String operator, required String endExpr, required String incExpr, required Map context, }) { int current = _evalToInt(startExpr, context) ?? 0; bool checkCondition(int curVal) { final endVal = _evalToInt(endExpr, context) ?? 0; switch (operator) { case '<': return curVal < endVal; case '<=': return curVal <= endVal; case '>': return curVal > endVal; case '>=': return curVal >= endVal; } return false; } /// Increments the current value based on the increment expression. /// /// This function processes the increment expression [incExpr] to determine /// how to modify the [curVal]. It supports the following increment patterns: /// - `varName++` or `varName--`: increments or decrements the value by 1. /// - `varName += n` or `varName -= n`: adds or subtracts the specified amount. /// - `varName = varName + n` or `varName = varName - n`: adds or subtracts the specified amount. /// /// If no pattern is matched, the function defaults to incrementing the value by 1. /// /// Returns the new value after applying the increment. int increment(int curVal) { final trimmed = incExpr.trim(); if (trimmed == '$varName++') { return curVal + 1; } else if (trimmed == '$varName--') { return curVal - 1; } final addSubPattern = RegExp(r'^' + varName + r'\s*([\+\-]=)\s*(\d+)$'); final addSubMatch = addSubPattern.firstMatch(trimmed); if (addSubMatch != null) { final op = addSubMatch.group(1)!; final amt = int.parse(addSubMatch.group(2)!); return (op == '+=') ? curVal + amt : curVal - amt; } final assignPattern = RegExp( r'^' + varName + r'\s*=\s*' + varName + r'\s*([\+\-])\s*(\d+)$', ); final assignMatch = assignPattern.firstMatch(trimmed); if (assignMatch != null) { final sign = assignMatch.group(1)!; final amt = int.parse(assignMatch.group(2)!); return (sign == '+') ? curVal + amt : curVal - amt; } return curVal + 1; } final buffer = StringBuffer(); while (checkCondition(current)) { final subCtx = {...context, varName: current}; buffer.write(TemplateEngine().renderString(loopBlock, subCtx)); current = increment(current); } return buffer.toString(); } /// Evaluates a string expression and attempts to convert it to an integer. /// /// This function processes the provided [expr] in the following ways: /// - Tries to parse [expr] directly as an integer. /// - Checks if the expression matches the pattern `.length`, and if /// the variable in the [context] is a list, returns its length. /// - If [expr] is a key in the [context] and its value is an integer, returns /// that integer. /// /// If none of the above conditions are met, the function returns `null`. /// /// **Parameters:** /// - [expr]: A string representing the expression to evaluate. /// - [context]: A map containing variable names and their corresponding values. /// /// **Returns:** /// An integer if the expression can be evaluated to an integer; otherwise, `null`. int? _evalToInt(String expr, Map context) { expr = expr.trim(); final maybeInt = int.tryParse(expr); if (maybeInt != null) return maybeInt; final dotPattern = RegExp(r'^(\w+)\.length$'); final dotMatch = dotPattern.firstMatch(expr); if (dotMatch != null) { final varName = dotMatch.group(1)!; final obj = context[varName]; if (obj is List) return obj.length; return null; } if (context.containsKey(expr) && context[expr] is int) { return context[expr] as int; } return null; } } ================================================ FILE: lib/src/view_engine/processor_engine/if_statement_processor.dart ================================================ import 'abs_processor.dart'; import 'evaluate_expression.dart'; import '../template_engine.dart'; /// A processor that handles `{@ if ... @}` if statements with /// optional `{@ elseif ... @}` and `{@ else @}` sections. /// /// Example: /// ```html /// {@ if is_admin @} /// You are admin /// {@ else @} /// Not admin /// {@ endif @} /// ``` /// nested if statements /// ```html /// {@ if is_admin @} /// {@ if name == 'Vania' @}Hello @{name}!{@ else @}Hello other admin{@ endif @} /// {@ else @} /// Not admin /// {@ endif @} /// ``` /// class IfStatementProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { context ??= {}; return _parseIfBlocks(content, context); } /// Parse and replace `{@ if ... @}` blocks in [template] with values from [context]. /// /// This implementation supports nested if statements. /// /// The replacement is done by expanding the block content if the condition is true. /// If the condition is false, the block content is not expanded. /// /// The following example: /// ```html /// {@ if is_admin @} /// You are admin /// {@ else @} /// Not admin /// {@ endif @} /// ``` /// Is replaced with "You are admin" if `is_admin` is true in the context, /// and "Not admin" if `is_admin` is false. /// String _parseIfBlocks(String template, Map context) { final buffer = StringBuffer(); int index = 0; while (true) { final startPos = template.indexOf('{@ if', index); if (startPos == -1) { buffer.write(template.substring(index)); break; } buffer.write(template.substring(index, startPos)); final ifStartClose = template.indexOf('@}', startPos); if (ifStartClose == -1) { buffer.write(template.substring(startPos)); break; } final ifConditionExpr = template .substring(startPos + 5, ifStartClose) .trim(); int blockStart = ifStartClose + 2; int searchPos = blockStart; int nested = 0; int endifPos = -1; while (true) { final nextIf = template.indexOf('{@ if', searchPos); final nextEndif = template.indexOf('{@ endif @}', searchPos); if (nextEndif == -1) { break; } if (nextIf != -1 && nextIf < nextEndif) { nested++; searchPos = nextIf + 1; } else { if (nested > 0) { nested--; searchPos = nextEndif + 1; } else { endifPos = nextEndif; break; } } } if (endifPos == -1) { buffer.write(template.substring(blockStart)); break; } final ifBlockContent = template.substring(blockStart, endifPos); final expanded = _expandIfBlock(ifConditionExpr, ifBlockContent, context); buffer.write(expanded); final endifClose = endifPos + '{@ endif @}'.length; index = endifClose; } return buffer.toString(); } /// Expands an if-else block by evaluating conditions and returning the appropriate content. /// /// This function processes a block of content containing `if`, `elseif`, and `else` conditions, /// using the provided context to evaluate each condition. It returns the rendered string /// for the first true condition or the `else` block if no conditions are true. /// /// - Parameters: /// - ifConditionExpr: The initial 'if' condition expression as a string. /// - ifBlockContent: The content of the block to be evaluated. /// - context: A map containing the context variables used for evaluating conditions. /// /// - Returns: A string with the expanded content for the first true condition or the `else` block. String _expandIfBlock( String ifConditionExpr, String ifBlockContent, Map context, ) { var cursor = 0; var currentCondition = ifConditionExpr; final segments = <_ConditionalSegment>[]; final elseIfRegex = RegExp(r'\{@\s*elseif\s+(.*?)\s*@\}'); final elseRegex = RegExp(r'\{@\s*else\s*@\}'); while (true) { final matchElseIf = elseIfRegex.firstMatch( ifBlockContent.substring(cursor), ); final matchElse = elseRegex.firstMatch(ifBlockContent.substring(cursor)); final elseIfPos = (matchElseIf == null) ? -1 : cursor + matchElseIf.start; final elsePos = (matchElse == null) ? -1 : cursor + matchElse.start; int nextPos = -1; bool isElseIf = false; if (elseIfPos == -1 && elsePos == -1) { } else if (elseIfPos == -1) { nextPos = elsePos; } else if (elsePos == -1) { nextPos = elseIfPos; isElseIf = true; } else { if (elseIfPos < elsePos) { nextPos = elseIfPos; isElseIf = true; } else { nextPos = elsePos; } } if (nextPos == -1) { final block = ifBlockContent.substring(cursor); segments.add( _ConditionalSegment( condition: currentCondition, content: block, isConditionSegment: true, ), ); break; } else { final block = ifBlockContent.substring(cursor, nextPos); segments.add( _ConditionalSegment( condition: currentCondition, content: block, isConditionSegment: true, ), ); if (isElseIf) { final elseIfMatch = elseIfRegex.firstMatch( ifBlockContent.substring(nextPos), ); if (elseIfMatch == null) break; currentCondition = elseIfMatch.group(1)!.trim(); cursor = nextPos + elseIfMatch.end; } else { final elseMatch = elseRegex.firstMatch( ifBlockContent.substring(nextPos), ); if (elseMatch == null) break; final elseStart = nextPos + elseMatch.end; final elseContent = ifBlockContent.substring(elseStart); segments.add( _ConditionalSegment( condition: '', content: elseContent, isConditionSegment: false, ), ); break; } } } for (final seg in segments) { if (seg.isConditionSegment) { if (evaluateExpression(seg.condition, context)) { return TemplateEngine().renderString(seg.content, context); } } else { return TemplateEngine().renderString(seg.content, context); } } return ''; } } class _ConditionalSegment { final String condition; final String content; final bool isConditionSegment; _ConditionalSegment({ required this.condition, required this.content, required this.isConditionSegment, }); } ================================================ FILE: lib/src/view_engine/processor_engine/include_processor.dart ================================================ import 'dart:convert'; import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; import 'package:vania/src/view_engine/template_engine.dart'; import 'package:vania/src/view_engine/template_reader.dart'; class IncludeProcessor implements AbsProcessor { /// Replaces `{@ include [, ] @}` blocks in [content] with the /// rendered content of the included file. /// /// The included file is read from the file system using the [FileTemplateReader] /// and rendered using [TemplateEngine] with a context that includes the /// variables from [context] as well as any additional data passed in the /// include tag. /// /// The additional data is expected to be a JSON object and is merged with /// the context from [context]. /// /// The included file's content is then replaced in the original content /// at the location of the include tag. /// /// The following example: /// @override String parse(String content, [Map? context]) { final includePattern = RegExp( r"\{@\s*include\(\s*'([^']+)'\s*(,\s*(\{.*?\}))?\)\s*@\}", dotAll: true, ); return content.replaceAllMapped(includePattern, (match) { final filePath = match.group(1) ?? ''; final rawData = match.group(3) ?? ''; final childContext = _parseIncludeData(rawData); final mergedContext = {...context ?? {}, ...childContext}; final includedTemplate = FileTemplateReader().read(filePath); return TemplateEngine().renderString(includedTemplate, mergedContext); }); } Map _parseIncludeData(String dataString) { dataString = dataString.trim(); if (dataString.isEmpty) return {}; try { final decoded = jsonDecode(dataString); if (decoded is Map) { return decoded; } } catch (_) {} return {}; } } ================================================ FILE: lib/src/view_engine/processor_engine/old_processor.dart ================================================ import 'package:vania/src/view_engine/template_engine.dart'; import 'abs_processor.dart'; class OldProcessor extends AbsProcessor { @override String parse(String content, [Map? context]) { final oldPattern = RegExp( r"\{@\s*old\(\s*'([^']*)'\s*\)\s*@\}", dotAll: true, ); content = content.replaceAllMapped(oldPattern, (oldMatch) { final oldKey = oldMatch.group(1); return TemplateEngine().formData[oldKey] ?? ''; }); return content; } } ================================================ FILE: lib/src/view_engine/processor_engine/route_processor.dart ================================================ import 'dart:convert'; import 'package:vania/src/exception/internal_server_error.dart'; import 'package:vania/src/route/router.dart'; import 'abs_processor.dart'; class RouteProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final routePattern = RegExp( r"(\/?)\{\@\s*route\(\s*\'([^']+)\'(?:,\s*({[^}]+}))?\s*\)\s*\@\}", dotAll: true, ); return content.replaceAllMapped(routePattern, (match) { String? slash = match.group(1); final routeName = match.group(2) ?? ''; final jsonParams = match.group(3) ?? ''; if (slash == null || slash.isEmpty) { slash = '/'; } List filteredRoute = Router().routes .where((e) => e.name?.toLowerCase() == routeName.toLowerCase()) .toList(); if (filteredRoute.isEmpty) { throw InternalServerError( message: 'Route $routeName not found', code: 500, ); } String path = filteredRoute.first.path; Map params = {}; if (jsonParams.isNotEmpty) { try { params = jsonDecode(jsonParams); } catch (e) { params = {}; } } if (params.isNotEmpty) { path = injectParams(path, params); } return "$slash$path"; }); } String injectParams(String template, Map params) { final placeholderPattern = RegExp(r'\{(\w+)\}'); return template.replaceAllMapped(placeholderPattern, (match) { final key = match.group(1); final value = params[key]; return value?.toString() ?? ''; }); } } ================================================ FILE: lib/src/view_engine/processor_engine/section_processor.dart ================================================ import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; class SectionProcessor implements AbsProcessor { /// Replace placeholders in a template string with values from a context. /// /// Replaces: /// - `{@ yield('section_name') @}` with the content of the section in [context] /// with the given name. If the section does not exist, an empty string is /// used. /// - `{@ section('section_name') @}...{@ show @}` with the content of the /// section in [context] with the given name. If the section does not exist, /// the content inside the section is used. /// @override String parse(String content, [Map? context]) { final yieldPattern = RegExp( r"\{@\s*yield\(\s*'([^']+)'\s*\)\s*@\}", dotAll: true, ); content = content.replaceAllMapped(yieldPattern, (match) { final sectionName = match.group(1) ?? ''; return context?[sectionName] ?? ''; }); final parentSectionPattern = RegExp( r"\{@\s*section\(\s*'([^']+)'\s*\)\s*@\}(.*?)\{@\s*show\s*\}", dotAll: true, ); content = content.replaceAllMapped(parentSectionPattern, (match) { final parentSectionName = match.group(1) ?? ''; final parentContent = match.group(2) ?? ''; final childContent = context?[parentSectionName]; return childContent ?? parentContent; }); return content; } /// Parse child sections from a template string. /// /// Given a template string, parse out all sections (both inline and block) and /// return them as a map with the section name as the key and the section content /// as the value. /// /// The following example: /// /// ```html /// {@ section section('content') @} ///

content

/// {@ endsection @} /// ``` /// Map parseChildSections(String childTemplate) { final sections = {}; final blockSectionPattern = RegExp( r"\{@\s*section\(\s*'([^']+)'\s*\)\s*@\}(.*?)\{@\s*endsection\s*@\}", dotAll: true, ); childTemplate = childTemplate.replaceAllMapped(blockSectionPattern, ( match, ) { final sectionName = match.group(1) ?? ''; final content = match.group(2) ?? ''; sections[sectionName] = content; return ''; }); final inlineSectionPattern = RegExp( r"\{@\s*section\(\s*'([^']+)'\s*,\s*'(.*?)'\s*\)\s*@\}", dotAll: true, ); childTemplate = childTemplate.replaceAllMapped(inlineSectionPattern, ( match, ) { final sectionName = match.group(1) ?? ''; final inlineContent = match.group(2) ?? ''; sections[sectionName] = inlineContent; return ''; }); return sections; } } ================================================ FILE: lib/src/view_engine/processor_engine/session_processor.dart ================================================ import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; import 'package:vania/src/view_engine/template_engine.dart'; class SessionProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final hasSessionPattern = RegExp( r"hasSession\(\s*'([^']*)'\s*\)", dotAll: true, ); content = content.replaceAllMapped(hasSessionPattern, (match) { final sessionKey = match.group(1); return TemplateEngine().sessions.containsKey(sessionKey).toString(); }); final sessionPattern = RegExp( r"\{@\s*session\(\s*'([^']*)'\s*\)\s*@\}", dotAll: true, ); content = content.replaceAllMapped(sessionPattern, (math) { final sessionKey = math.group(1); return TemplateEngine().sessions[sessionKey] ?? ''; }); return content; } } ================================================ FILE: lib/src/view_engine/processor_engine/switch_cases_processor.dart ================================================ import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; // {@ switch @} // {@ case @}{@ endcase @} // {@ default @}{@ enddefault @} (optional) // {@ endswitch @} class SwitchCasesProcessor implements AbsProcessor { /// Parse `{@ switch @}` blocks in [content] and replace them with /// values from [context]. // /// The replacement is done by expanding the matching case content if the /// condition is true. If no case matches, the default content is expanded if /// it is provided. // /// The following example: /// @override String parse(String content, [Map? context]) { context ??= {}; final switchPattern = RegExp( r'\{@\s*switch\s+(.*?)\s*@\}(.*?)\{@\s*endswitch\s*@\}', dotAll: true, ); return content.replaceAllMapped(switchPattern, (match) { final switchVariable = match.group(1)?.trim() ?? ''; final switchContent = match.group(2) ?? ''; final switchValue = context![switchVariable]; final casePattern = RegExp( r'\{@\s*case\s+(.*?)\s*@\}(.*?)\{@\s*endcase\s*@\}', dotAll: true, ); final defaultPattern = RegExp( r'\{@\s*default\s*@\}(.*?)\{@\s*enddefault\s*@\}', dotAll: true, ); for (final caseMatch in casePattern.allMatches(switchContent)) { final caseValueRaw = caseMatch.group(1)?.trim() ?? ''; final caseContent = caseMatch.group(2) ?? ''; final caseValues = caseValueRaw.split(',').map((val) => val.trim()); for (final value in caseValues) { final parsedCaseValue = num.tryParse(value) ?? value; final parsedSwitchValue = (switchValue != null) ? num.tryParse(switchValue.toString()) ?? switchValue : null; if (parsedSwitchValue == parsedCaseValue) { return caseContent; } } } final defaultMatch = defaultPattern.firstMatch(switchContent); if (defaultMatch != null) { return defaultMatch.group(1)!; } return ''; }); } } ================================================ FILE: lib/src/view_engine/processor_engine/translate_processor.dart ================================================ import 'dart:convert'; import 'package:vania/src/utils/helper.dart'; import 'abs_processor.dart'; class TranslateProcessor implements AbsProcessor { @override String parse(String content, [Map? context]) { final translatePattern = RegExp( r"\{\@\s*trans\(\s*'([^']+)'\s*" r"(?:,\s*({[^}]+}))?\s*" r"(?:,\s*([^)]+?))?\s*" r"\)\s*\@\}", dotAll: true, ); return content.replaceAllMapped(translatePattern, (match) { final key = match.group(1) ?? ''; Map args = {}; final rawArgs = match.group(2); if (rawArgs != null) { try { args = jsonDecode(rawArgs) as Map; } catch (_) {} } final stripQuotes = RegExp(r"""^['"]|['"]$"""); String? locale; final rawLocale = match.group(3); if (rawLocale != null) { locale = rawLocale.trim().replaceAll(stripQuotes, ''); } return trans(key, args: args, locale: locale); }); } } ================================================ FILE: lib/src/view_engine/processor_engine/variables_processor.dart ================================================ import 'abs_processor.dart'; import 'dart:math'; class VariablesProcessor implements AbsProcessor { static final _variablePattern = RegExp(r'@\{(.*?)\}', dotAll: true); /// Replaces placeholders in the form of `@{expression}` with the evaluated value of [expression] in the context of [context]. /// /// The following are valid expressions: /// /// - A variable name, e.g. `@{name}` /// /// If the expression evaluates to a non-string value, it is converted to a string. /// /// If the expression is invalid, or if the context does not contain a value for the specified variable, /// an empty string is returned. /// @override String parse(String content, [Map? context]) { context = context ?? {}; return content.replaceAllMapped(_variablePattern, (match) { final rawExpression = match.group(1)?.trim() ?? ''; if (rawExpression.isEmpty) return ''; if (rawExpression.contains('|')) { return _handleVariableWithFilters(rawExpression, context ?? {}); } if (_looksLikeVariablePath(rawExpression)) { final value = _fetchValueWithBracketNotation( rawExpression, context ?? {}, ); if (value != null) return value.toString(); } final exprValue = _evaluateExpression(rawExpression, context ?? {}); return exprValue?.toString() ?? ''; }); } /// Processes a variable expression with filters from the template content. /// /// This function splits the [rawExpression] into a variable name and filter /// operations. It fetches the variable value from the [context] using bracket /// notation, then sequentially applies each filter to the value. /// /// The filters are applied in the order they appear in the expression, and each /// filter transforms the value. If no filters are provided, the function returns /// the string representation of the variable's value. If the variable is not found /// in the context or an error occurs in fetching or filtering, an empty string is returned. /// /// - [rawExpression]: The raw expression containing the variable and optional filters. /// - [context]: The context map with variable values available for substitution. /// /// Returns the filtered variable value as a string. String _handleVariableWithFilters( String rawExpression, Map context, ) { final parts = rawExpression.split('|').map((e) => e.trim()).toList(); final variableName = parts.first; final filters = parts.length > 1 ? parts.sublist(1) : []; dynamic value = _fetchValueWithBracketNotation(variableName, context); for (final filter in filters) { value = _applyFilter(value, filter); } return value?.toString() ?? ''; } bool _looksLikeVariablePath(String expr) { return expr.contains('.') || expr.contains('['); } /// Fetches a value from a [context] given a string expression. /// /// The [expression] can contain bracket notation, e.g. `user.name` or /// `users[0].name`. The value is fetched by splitting the expression into /// segments and resolving each segment on the current value. /// /// If the expression contains bracket index notation, e.g. `users[0]`, the /// bracket index is resolved to its value in the context by calling /// [_resolveBracketIndexVars]. /// /// The function returns `null` if any segment in the expression is `null`. /// dynamic _fetchValueWithBracketNotation( String expression, Map context, ) { expression = _resolveBracketIndexVars(expression, context); final segments = expression.split('.'); dynamic currentValue = context; for (final segment in segments) { currentValue = _resolveSegment(currentValue, segment); if (currentValue == null) return null; } return currentValue; } String _resolveBracketIndexVars( String expression, Map context, ) { final bracketVarRegex = RegExp(r'\[([^\[\]]+)\]'); return expression.replaceAllMapped(bracketVarRegex, (m) { final inside = m.group(1)!; final asInt = int.tryParse(inside); if (asInt != null) { return '[$asInt]'; } if (context.containsKey(inside) && context[inside] is int) { return '[${context[inside]}]'; } return '[$inside]'; }); } /// Resolves a segment of a dot-separated expression on [currentValue]. /// /// If [currentValue] is a map, the segment is resolved to a value in the map. /// If the segment is a bracket-index expression, e.g. `users[0]`, the /// expression is resolved to the value at the specified index in the list /// value associated with the key. If the key is not found, or the value is /// not a list, `null` is returned. /// /// If the segment is not a bracket-index expression, the segment is resolved /// to the value associated with the segment key. If the key is not found, /// `null` is returned. /// /// If [currentValue] is not a map, `null` is returned. dynamic _resolveSegment(dynamic currentValue, String segment) { if (currentValue == null) return null; if (currentValue is Map) { final bracketRegex = RegExp(r'^(\w+)\[(\d+)\]$'); final match = bracketRegex.firstMatch(segment); if (match != null) { final mapKey = match.group(1)!; final indexStr = match.group(2)!; if (!currentValue.containsKey(mapKey)) return null; final listObj = currentValue[mapKey]; if (listObj is List) { final idx = int.parse(indexStr); if (idx < 0 || idx >= listObj.length) return null; return listObj[idx]; } return null; } else { if (!currentValue.containsKey(segment)) return null; return currentValue[segment]; } } return null; } /// Applies a filter to a value, and returns the filtered value. /// /// The available filters are: /// /// - `default:`: If the value is null, returns ``. /// - `join:`: If the value is a list, joins the elements with ``. /// - `uppercase`: If the value is a string, returns its uppercase version. /// - `lowercase`: If the value is a string, returns its lowercase version. /// /// Otherwise, returns the original value. dynamic _applyFilter(dynamic value, String filter) { final defaultPattern = RegExp(r'^default:\s*(.*)$'); if (defaultPattern.hasMatch(filter)) { if (value == null) { final match = defaultPattern.firstMatch(filter); var defaultVal = match?.group(1)?.trim() ?? ''; defaultVal = defaultVal.replaceAll(RegExp(r'^"|"$'), ''); defaultVal = defaultVal.replaceAll(RegExp(r"^'|'$"), ''); return defaultVal; } return value; } final joinPattern = RegExp(r'^join:\s*(.*)$'); if (joinPattern.hasMatch(filter)) { if (value is List) { final match = joinPattern.firstMatch(filter); var delimiter = match?.group(1)?.trim() ?? ','; delimiter = delimiter.replaceAll(RegExp(r'^"|"$'), ''); delimiter = delimiter.replaceAll(RegExp(r"^'|'$"), ''); return value.join(delimiter); } return value; } if (filter == 'uppercase') { if (value is String) return value.toUpperCase(); return value; } if (filter == 'lowercase') { if (value is String) return value.toLowerCase(); return value; } return value; } /// Evaluates an expression in the given context. /// /// The expression can contain: /// /// - ternary expressions: `cond ? trueVal : falseVal` /// - comparison operators: `==`, `!=`, `>=`, `<=`, `>`, `<` /// - arithmetic operators: `+`, `-`, `*`, `/`, `%`, `^` /// - any valid Dart expression /// /// The context is used to resolve any variables used in the expression. /// /// Returns the result of the expression, or `null` if the expression is invalid. dynamic _evaluateExpression(String expr, Map context) { expr = expr.trim(); final ternaryPattern = RegExp(r'^(.+?)\?(.*?)\:(.*)$'); final tMatch = ternaryPattern.firstMatch(expr); if (tMatch != null) { final condRaw = tMatch.group(1)!.trim(); final trueRaw = tMatch.group(2)!.trim(); final falseRaw = tMatch.group(3)!.trim(); final condVal = _evaluateExpression(condRaw, context); final boolCond = (condVal is bool) ? condVal : _boolFromAnything(condVal); if (boolCond) { return _evaluateExpression(trueRaw, context); } else { return _evaluateExpression(falseRaw, context); } } final comparisonPattern = RegExp(r'(.+?)(==|!=|>=|<=|>|<)(.+)'); final compMatch = comparisonPattern.firstMatch(expr); if (compMatch != null) { final leftRaw = compMatch.group(1)!.trim(); final op = compMatch.group(2)!.trim(); final rightRaw = compMatch.group(3)!.trim(); final leftVal = _evalOperand(leftRaw, context); final rightVal = _evalOperand(rightRaw, context); return _compareValues(leftVal, rightVal, op); } final arithmeticPattern = RegExp(r'(.+?)(\+|\-|\*|\/|\%|\^)(.+)'); final arithMatch = arithmeticPattern.firstMatch(expr); if (arithMatch != null) { final leftRaw = arithMatch.group(1)!.trim(); final op = arithMatch.group(2)!.trim(); final rightRaw = arithMatch.group(3)!.trim(); final leftVal = _evalOperand(leftRaw, context); final rightVal = _evalOperand(rightRaw, context); return _arithValues(leftVal, rightVal, op); } return _evalOperand(expr, context); } dynamic _evalOperand(String raw, Map context) { final asInt = int.tryParse(raw); if (asInt != null) return asInt; final asDouble = double.tryParse(raw); if (asDouble != null) return asDouble; if (raw == 'true') return true; if (raw == 'false') return false; final val = _fetchValueWithBracketNotation(raw, context); if (val != null) return val; return raw; } bool _compareValues(dynamic left, dynamic right, String operator) { if (left is num && right is num) { switch (operator) { case '==': return left == right; case '!=': return left != right; case '>': return left > right; case '>=': return left >= right; case '<': return left < right; case '<=': return left <= right; } } final lstr = left?.toString() ?? ''; final rstr = right?.toString() ?? ''; switch (operator) { case '==': return lstr == rstr; case '!=': return lstr != rstr; case '>': return lstr.compareTo(rstr) > 0; case '>=': return lstr.compareTo(rstr) >= 0; case '<': return lstr.compareTo(rstr) < 0; case '<=': return lstr.compareTo(rstr) <= 0; } return false; } dynamic _arithValues(dynamic left, dynamic right, String operator) { if (left is num && right is num) { switch (operator) { case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return right == 0 ? null : left / right; case '%': return right == 0 ? null : left % right; case '^': return pow(left, right); } } if (operator == '+') { return '${left?.toString() ?? ''}${right?.toString() ?? ''}'; } return null; } /// Convert any value to a boolean. /// /// If the value is already a boolean, it is returned as is. /// /// If the value is a string, it is converted to lower case and compared to /// "true" and "false". If it matches one of those, the corresponding boolean /// is returned. Otherwise, `false` is returned. /// /// If the value is a number, it is converted to a boolean by checking if it /// is not equal to 0. /// /// For all other values, `null` is converted to `false`, and all other values /// are converted to `true`. bool _boolFromAnything(dynamic val) { if (val is bool) return val; if (val is String) { final lower = val.toLowerCase(); if (lower == 'true') return true; if (lower == 'false') return false; } if (val is num) { return val != 0; } return val != null; } } ================================================ FILE: lib/src/view_engine/template_engine.dart ================================================ import 'package:vania/src/view_engine/processor_engine/abs_processor.dart'; import 'package:vania/src/view_engine/processor_engine/variables_processor.dart'; import 'processor_engine/assets_processor.dart'; import 'processor_engine/comment_processor.dart'; import 'processor_engine/csrf_processor.dart'; import 'processor_engine/csrf_token_processor.dart'; import 'processor_engine/error_processor.dart'; import 'processor_engine/if_statement_processor.dart'; import 'processor_engine/extends_processor.dart'; import 'processor_engine/for_loop_processor.dart'; import 'processor_engine/include_processor.dart'; import 'processor_engine/old_processor.dart'; import 'processor_engine/route_processor.dart'; import 'processor_engine/section_processor.dart'; import 'processor_engine/session_processor.dart'; import 'processor_engine/switch_cases_processor.dart'; import 'processor_engine/translate_processor.dart'; import 'template_reader.dart'; class _TemplateProcessingPipeline { final List _processors; _TemplateProcessingPipeline(this._processors); String run(String content, Map data) { for (final processor in _processors) { content = processor.parse(content, data); } return content; } } class TemplateEngine { static final TemplateEngine _singleton = TemplateEngine._internal(); factory TemplateEngine() => _singleton; TemplateEngine._internal(); final SectionProcessor _sectionProcessor = SectionProcessor(); final Map sessionErrors = {}; final Map formData = {}; final Map sessions = {}; String render(String template, [Map? data]) { String templateContent = FileTemplateReader().read(template); String renderedTemplate = renderString(templateContent, data); sessionErrors.clear(); formData.clear(); sessions.clear(); return renderedTemplate; } /// Renders a template string with the provided data context. /// /// This function processes the template content by running it through a pipeline /// of processors, including extends, include, section, for loop, switch case, /// conditional, and variables processors. Each processor modifies the template /// content based on the provided context data. /// /// The context data is first merged with any child sections parsed from the template content. /// /// Returns the fully rendered content as a string. /// /// Parameters: /// - [templateContent]: The raw template content to be rendered. /// - [data] (optional): A map of context data to be used for rendering the template. /// If not provided, an empty map is used. /// String renderString(String templateContent, [Map? data]) { data = { ...data ?? {}, ..._sectionProcessor.parseChildSections(templateContent), }; final pipeline = _TemplateProcessingPipeline([ ExtendsProcessor(), _sectionProcessor, ErrorProcessor(), SessionProcessor(), ForLoopProcessor(), SwitchCasesProcessor(), IfStatementProcessor(), VariablesProcessor(), CsrfProcessor(), CsrfTokenProcessor(), OldProcessor(), TranslateProcessor(), CommentProcessor(), RouteProcessor(), AssetsProcessor(), IncludeProcessor(), ]); final renderedContent = pipeline.run(templateContent, data); return renderedContent; } } ================================================ FILE: lib/src/view_engine/template_reader.dart ================================================ import 'dart:io'; /// An interface (abstract class) for reading template files. abstract class TemplateReader { /// Reads the template contents from the given [filePath]. String read(String filePath); } class FileTemplateReader implements TemplateReader { static final FileTemplateReader _singleton = FileTemplateReader._internal(); factory FileTemplateReader() => _singleton; FileTemplateReader._internal(); /// Reads the html template from the given [template] path. /// /// The template path is relative to the `lib/resources/view/` directory. /// The template file must end with `.html`. /// /// Throws a [FileSystemException] if the file does not exist. /// @override String read(String template) { final filePath = 'lib/resources/view/$template.html'; File file = File(filePath); if (!file.existsSync()) { file = File('$template.html'); if (!file.existsSync()) { throw FileSystemException('Html template not found', filePath); } } return file.readAsStringSync(); } } ================================================ FILE: lib/src/websocket/web_socket_handler.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:uuid/v8.dart'; import 'package:vania/src/http/middleware/middleware.dart'; import 'package:vania/src/http/middleware/web_socket_middleware_handler.dart'; import 'websocket_client.dart'; import 'websocket_constants.dart'; import 'websocket_event.dart'; import 'websocket_session.dart'; class WebSocketHandler implements WebSocketEvent { final WebsocketSession _session = WebsocketSession(); static final WebSocketHandler _singleton = WebSocketHandler._internal(); factory WebSocketHandler() => _singleton; WebSocketHandler._internal(); final Map?> _middleware = {}; late String _websocketRoute; WebSocketHandler websocketRoute( String path, { List? middleware, }) { _websocketRoute = path.replaceFirst('/', ''); _middleware[_websocketRoute] = middleware; return this; } final Map _events = {}; Future handler(HttpRequest req) async { String routePath = req.uri.path.replaceFirst('/', ''); WebSocket websocket = await WebSocketTransformer.upgrade(req); String sessionId = 'ws:${UuidV8().generate()}'; _session.addNewSession(sessionId, websocket); final WebSocketClientImpl client = WebSocketClientImpl( session: _session, id: sessionId, routePath: routePath, ); websocket.add( jsonEncode({ 'event': 'connected', 'payload': {'session_id': sessionId}, }), ); try { if (_middleware[_websocketRoute] != null) { await webSocketMiddlewareHandler( _middleware[_websocketRoute] as List, req, ); } } on WebSocketException catch (e) { websocket.add( jsonEncode({ 'event': 'error', 'payload': {'message': e.message}, }), ); return; } Function? openFunction = _events['${routePath}_connect']; if (openFunction != null) { Function.apply(openFunction, [client]); } websocket.listen( (data) async { Map payload = jsonDecode(data); String event = '${routePath}_${payload[webScoketEventKey]}'; /// client join the room if (event == '${routePath}_$webSocketJoinRoomEventName') { String? roomId = payload[webSocketRoomKey].toString(); if (roomId.isNotEmpty) { _session.joinRoom(sessionId, '${routePath}_$roomId'); } return; } /// client left the room if (event == webSocketLeftRoomEventName) { String? roomId = payload[webSocketRoomKey].toString(); if (roomId.isNotEmpty) { _session.leftRoom(sessionId, '${routePath}_$roomId'); } return; } /// websocket response /// ``` /// event.on('event',function(WebSocketClient client,message){ /// response /// }); /// ``` dynamic message = payload[webSocketMessageKey]; Function? controller = _events[event]; if (controller == null) { return; } Function.apply(controller, [client, message]); }, onDone: () { Function? openFunction = _events['${routePath}_disconnect']; if (openFunction != null) { Function.apply(openFunction, [client]); } _session.removeSession(sessionId); }, onError: (_) { Function? openFunction = _events['${routePath}_error']; if (openFunction != null) { Function.apply(openFunction, [client]); } _session.removeSession(sessionId); }, ); } /// Listener /// ``` /// event.on('event',function(WebSocketClient client,message){ /// response /// }); /// ``` @override void on(String event, Function function) { _events['${_websocketRoute}_$event'] = function; } } ================================================ FILE: lib/src/websocket/websocket_client.dart ================================================ import 'dart:convert'; import 'websocket_session.dart'; abstract class WebSocketClient { const WebSocketClient(); String get clientId; List get activeSessions; List getRoomMembers({String roomId = ''}); bool isActiveSession({String sessionId = ''}); String get activeRoom; String get previousRoom; void emit(String event, dynamic payload); void toRoom(String event, String room, dynamic payload); void broadcast(String event, dynamic payload); void to(String clientId, String event, dynamic payload); } class WebSocketClientImpl implements WebSocketClient { final String id; final String routePath; final WebsocketSession session; const WebSocketClientImpl({ required this.session, required this.id, required this.routePath, }); @override String get clientId => id; @override List get activeSessions => session.getActiveSessionIds(); /// emit to self sender /// ``` /// event.emit('event',payload) /// ``` @override void emit(String event, dynamic payload) { SessionInfo? info = session.getWebSocketInfo(id); if (info != null) { info.websocket.add(jsonEncode({'event': event, 'payload': payload})); } } /// emit to room all users in room can see this message /// exclude sender /// ``` /// event.toRoom('event','room',payload) /// ``` @override void toRoom(String event, String room, dynamic payload) { String roomId = room.replaceFirst('ws_', ''); List members = session.getRoomMembers('${routePath}_$roomId'); for (String member in members) { SessionInfo? info = session.getWebSocketInfo(member); if (info != null) { info.websocket.add(jsonEncode({'event': event, 'payload': payload})); } } } /// emit to specific seesion id /// ``` /// event.to(clientId,'event',payload) /// ``` @override void to(String clientId, String event, dynamic payload) { SessionInfo? info = session.getWebSocketInfo(clientId); if (info != null) { info.websocket.add(jsonEncode({'event': event, 'payload': payload})); } } /// broadcast to all connected sessions exclude sender ///``` /// event.broadcast('event',payload) /// ``` @override void broadcast(String event, dynamic payload) { List sessions = session.getActiveSessions(); sessions.removeWhere((item) => item.sessionId == id); sessions.shuffle(); for (SessionInfo session in sessions) { session.websocket.add(jsonEncode({'event': event, 'payload': payload})); } } void joinRoom(String roomId) { toRoom("join-room", roomId, "join room"); } void leftRoom(String roomId) { toRoom("left-room", roomId, "left room"); } @override List getRoomMembers({String roomId = ''}) => session.getRoomMembers(roomId); @override String get activeRoom => session.getWebSocketInfo(id)?.activeRoom ?? ''; @override String get previousRoom => session.getWebSocketInfo(id)?.previousRoom ?? ''; @override bool isActiveSession({String sessionId = ''}) => session.isActiveSession(sessionId); } ================================================ FILE: lib/src/websocket/websocket_constants.dart ================================================ const String webSocketJoinRoomEventName = 'join-room'; const String webSocketLeftRoomEventName = 'left-room'; const String webScoketEventKey = 'event'; const String webSocketMessageKey = 'payload'; const String webSocketSenderKey = 'sender'; const String webSocketRoomKey = 'room'; ================================================ FILE: lib/src/websocket/websocket_event.dart ================================================ abstract class WebSocketEvent { void on(String event, Function function); } ================================================ FILE: lib/src/websocket/websocket_session.dart ================================================ import 'dart:collection'; import 'dart:io'; final Map _activeSessions = HashMap(); final Map> _rooms = >{}; class SessionInfo { final String sessionId; final WebSocket websocket; String? activeRoom; String? previousRoom; SessionInfo({ required this.sessionId, required this.websocket, this.activeRoom, this.previousRoom, }); } class WebsocketSession { static final WebsocketSession _singleton = WebsocketSession._internal(); factory WebsocketSession() { return _singleton; } WebsocketSession._internal(); /// add new websocket session to the active sessions void addNewSession(String sessionId, WebSocket ws) { _activeSessions.addAll({ sessionId: SessionInfo(sessionId: sessionId, websocket: ws), }); } /// get session of connected socket SessionInfo? getWebSocketInfo(String sessionId) { return _activeSessions[sessionId]; } /// remove session of connected socket void removeSession(String sessionId) { SessionInfo? info = _activeSessions[sessionId]; if (info != null) { leftRoom(sessionId, info.activeRoom); _activeSessions.remove(sessionId); info.websocket.close(); } } void joinRoom(String sessionId, String roomId) { if (_rooms[roomId] == null) { _rooms[roomId] = []; } _rooms[roomId]?.add(sessionId); SessionInfo? info = _activeSessions[sessionId]; if (info != null) { if (info.previousRoom != null) { leftRoom(sessionId, info.previousRoom); } info.activeRoom = roomId; info.activeRoom = roomId; } } void leftRoom(String sessionId, String? roomId) { if (_rooms[roomId] != null && roomId != null) { SessionInfo? info = _activeSessions[sessionId]; if (info != null) { info.previousRoom = null; info.activeRoom = null; _rooms[roomId]?.remove(sessionId); } } } /// get all room members (socket ids) /// for send message to a room List getRoomMembers(String roomId) { return _rooms[roomId] ?? []; } bool isRoom(String roomId) { return _rooms[roomId] != null ? true : false; } bool isActiveSession(String sessionId) { return _activeSessions[sessionId] != null ? true : false; } List getActiveSessions() { return _activeSessions.values.toList(); } List getActiveSessionIds() { return _activeSessions.keys.toList(); } } ================================================ FILE: lib/vania.dart ================================================ export 'src/extensions/extensions.dart'; export 'src/redis/vania_redis.dart'; export 'src/cache/redis_cache_driver.dart'; export 'src/cache/cache_driver.dart'; export 'src/cache/cache.dart'; export 'src/storage/storage_driver.dart'; export 'src/storage/storage.dart'; export 'src/utils/helper.dart'; export 'src/env_handler/env.dart'; export 'src/logger/logger.dart'; export 'src/config/config.dart'; export 'src/cryptographic/hash.dart'; export 'src/exception/base_http_exception.dart'; export 'src/exception/database_exception.dart'; export 'src/exception/exception_handler.dart'; export 'src/exception/forbidden_exception.dart'; export 'src/exception/http_exception.dart'; export 'src/exception/internal_server_error.dart'; export 'src/exception/invalid_argument_exception.dart'; export 'src/exception/not_found_exception.dart'; export 'src/exception/page_expired_exception.dart'; export 'src/exception/query_exception.dart'; export 'src/exception/redirect_exception.dart'; export 'src/exception/throttle_exception.dart'; export 'src/exception/unauthenticated.dart'; export 'src/exception/unauthorized_exception.dart'; export 'src/exception/validation_exception.dart'; export 'application.dart'; ================================================ FILE: lib/websocket.dart ================================================ export 'src/websocket/websocket_client.dart'; export 'src/websocket/websocket_event.dart'; ================================================ FILE: pubspec.yaml ================================================ name: vania description: Fast, simple, and powerful backend framework for Dart built with version: 1.1.1 homepage: https://vdart.dev repository: https://github.com/vania-dart/framework issue_tracker: https://github.com/vania-dart/framework/issues documentation: https://vdart.dev/docs/intro topics: [server, backend, httpserver, framework, web] screenshots: - description: 'Vania web application' path: assets/logo.png environment: sdk: '>=3.9.0 <4.0.0' dependencies: args: ^2.7.0 crypto: ^3.0.6 cryptography: ^2.7.0 dart_jsonwebtoken: ^3.3.1 mailer: ^6.5.0 meta: ^1.17.0 mime: ^2.0.0 mysql_client: ^0.0.27 path: ^1.9.1 postgres: ^3.5.9 sprintf: ^7.0.0 sqlite3: ^2.9.3 string_scanner: ^1.4.1 uuid: ^4.5.1 dev_dependencies: lints: ^6.0.0 test: ^1.26.2 http: ^1.4.0 http_parser: ^4.1.2 build_runner: ^2.6.0 mockito: ^5.4.6 ================================================ FILE: test/src/extensions/date_time_extension_test.dart ================================================ import 'package:test/test.dart'; import 'package:vania/src/extensions/date_time_extension.dart'; void main() { group('DateTimeExtension Tests', () { test('toAwsFormat produces correct AWS timestamp format', () { var dateTime = DateTime(2023, 1, 2, 3, 4, 5); expect(dateTime.toAwsFormat(), equals('20230102T030405Z')); }); test('format produces correctly formatted date-time string', () { var dateTime = DateTime(2023, 1, 2, 3, 4, 5); expect(dateTime.format(), equals('2023-01-02 03:04:05')); }); test('toAwsFormat handles single-digit and double-digit fields', () { var dateTime = DateTime(2023, 11, 20, 10, 9, 8); expect(dateTime.toAwsFormat(), equals('20231120T100908Z')); }); test('format handles edge case of leap year date', () { var dateTime = DateTime(2020, 2, 29, 23, 59, 59); expect(dateTime.format(), equals('2020-02-29 23:59:59')); }); }); } ================================================ FILE: test/src/extensions/number_extension_test.dart ================================================ import 'package:test/test.dart'; import 'package:vania/src/extensions/number_extension.dart'; void main() { group('NumberExtension.toFixed', () { test('should round a double to 2 decimal places', () { expect(123.456.toFixed(2), equals(123.46)); }); test('should round a double to 0 decimal places', () { expect(123.456.toFixed(0), equals(123)); }); test('should handle integers correctly', () { expect(123.toFixed(0), equals(123)); expect(123.toFixed(2), equals(123.00)); }); test('should handle rounding up correctly', () { expect(123.556.toFixed(2), equals(123.56)); }); test('should handle rounding down correctly', () { expect(123.554.toFixed(2), equals(123.55)); }); test('should process very small decimals', () { expect(0.000123456.toFixed(4), equals(0.0001)); }); test('should return a double when rounding integers with decimals', () { var result = 100.toFixed(2); expect(result, equals(100.00)); expect(result, isA()); }); test( 'should return an integer when rounding integers with zero decimals', () { var result = 100.toFixed(0); expect(result, equals(100)); expect(result, isA()); }, ); test('should handle negative numbers correctly', () { expect((-123.456).toFixed(2), equals(-123.46)); expect((-123.456).toFixed(0), equals(-123)); }); }); } ================================================ FILE: test/src/extensions/string_extension_test.dart ================================================ import 'package:test/test.dart'; import 'package:vania/src/extensions/string_extension.dart'; void main() { group("toInt extension test", () { test("should return int value", () { expect("1".toInt(), 1); }); test("should return null when it is string", () { expect("a".toInt(), null); }); test("should return null when it is double", () { expect("1,25".toInt(), null); }); }); } ================================================ FILE: test/src/extensions/string_list_extension_test.dart ================================================ import 'package:test/test.dart'; import 'package:vania/src/extensions/string_list_extension.dart'; void main() { group('List.joinWithAnd', () { test('joins empty list into empty string', () { expect([].joinWithAnd(), equals('')); }); test('handles single element list', () { expect(['apple'].joinWithAnd(), equals('apple')); }); test('joins two elements list with custom conjunction', () { expect(['apple', 'orange'].joinWithAnd(), equals('apple and orange')); }); test('joins multiple elements with default separator and conjunction', () { expect( ['apple', 'orange', 'banana'].joinWithAnd(), equals('apple, orange and banana'), ); }); test('allows custom separator and conjunction', () { expect( ['apple', 'orange', 'banana'].joinWithAnd(' / ', 'or'), equals('apple / orange or banana'), ); }); test('considers only the last two elements for conjunction', () { expect( ['apple', 'orange', 'banana', 'mango'].joinWithAnd(), equals('apple, orange, banana and mango'), ); }); }); } ================================================ FILE: test/unit/hash_test.dart ================================================ import 'dart:io'; import 'package:test/test.dart'; import 'package:vania/vania.dart'; void main() { group('Hash class test', () { setUp(() { Env().load(file: File('test/.env')); }); test('Make/Verify correct paswword', () { String password = "123456789"; String hash = Hash().make(password); expect(Hash().verify(password, hash), true); }); test('Make/Verify wrong password', () { String password = "123456789"; String hash = Hash().make(password); expect(Hash().verify("12345678", hash), false); }); }); } ================================================ FILE: test/unit/route_test.dart ================================================ import 'package:test/test.dart'; void main() { group('Route Test', () {}); } ================================================ FILE: test/unit/validation_test.dart ================================================ import 'package:test/test.dart'; import 'package:vania/src/http/validation/validator.dart'; void main() { group('Validation Test', () { test('Validation required Test', () { Validator validator = Validator(data: {'name': ''}); validator.validate({'name': 'required'}).then((_) { expect(validator.errors['name'], 'The name is required'); }); }); test('Validation integer Test', () { Validator validator = Validator(data: {'age': 'String'}); validator.validate({'age': 'integer'}).then((_) { expect(validator.errors['age'], 'The age must be an integer'); }); }); test('Validation not_in', () { Validator validator = Validator(data: {'status': 'active'}); validator.validate({'status': 'not_in:active,pending'}).then((_) { expect(validator.errors['status'], 'The status field cannot be active'); }); }); test('Validation start_with', () { Validator validator = Validator(data: {'name': 'Vania'}); validator.validate({'name': 'start_with:_va'}).then((_) { expect(validator.errors['name'], 'The name must start with _va'); }); }); test('Validation password confirmed', () { Validator validator = Validator( data: {'password': '12345678', 'password_confirmation': '112233445566'}, ); validator.validate({'password': 'confirmed'}).then((_) { expect(validator.errors['password'], 'The two password did not match'); }); }); test('Validation required if', () { Validator validator = Validator(data: {'username': '', 'type': 'login'}); validator.validate({'username': 'required_if:type,login'}).then((_) { expect(validator.errors['username'], 'The username is required'); }); }); }); }