├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yaml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yml ├── .gitignore ├── .pubignore ├── .vscode └── settings.json ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── docker-compose.yaml ├── docs.json ├── docs ├── assets │ ├── .DS_Store │ └── logo.png ├── core │ ├── controllers.mdx │ ├── middlewares.mdx │ ├── req-res-next.mdx │ ├── service-providers.mdx │ └── validation.mdx ├── getting-started.mdx ├── index.mdx └── orm │ └── quickstart.mdx ├── e2e_test.sh ├── lib ├── src │ ├── config.dart │ ├── database │ │ ├── database.dart │ │ ├── driver │ │ │ ├── driver.dart │ │ │ ├── mysql_driver.dart │ │ │ ├── pgsql_driver.dart │ │ │ └── sqlite_driver.dart │ │ └── entity │ │ │ ├── converter.dart │ │ │ ├── entity.dart │ │ │ ├── joins.dart │ │ │ ├── misc.dart │ │ │ └── relations.dart │ ├── migration.dart │ ├── primitives │ │ ├── serializer.dart │ │ └── where.dart │ ├── query │ │ ├── aggregates.dart │ │ └── query.dart │ ├── reflection.dart │ └── utils.dart └── yaroorm.dart ├── melos.yaml ├── melos_yaroorm.iml ├── packages ├── yaroorm_cli │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── analysis_options.yaml │ ├── bin │ │ └── yaroorm_cli.dart │ ├── build.yaml │ ├── lib │ │ ├── src │ │ │ ├── builder │ │ │ │ ├── generator.dart │ │ │ │ └── utils.dart │ │ │ ├── commands │ │ │ │ ├── command.dart │ │ │ │ ├── create_migration.dart │ │ │ │ ├── init_orm_command.dart │ │ │ │ ├── migrate_command.dart │ │ │ │ ├── migrate_fresh_command.dart │ │ │ │ ├── migrate_reset_command.dart │ │ │ │ └── migrate_rollback_command.dart │ │ │ ├── misc │ │ │ │ ├── migration.dart │ │ │ │ ├── migration.g.dart │ │ │ │ └── utils.dart │ │ │ └── yaroorm_cli_base.dart │ │ └── yaroorm_cli.dart │ ├── melos_yaroorm_cli.iml │ ├── pubspec.yaml │ └── pubspec_overrides.yaml └── yaroorm_test │ ├── .gitignore │ ├── database │ └── migrations │ │ ├── 2024_04_21_003612_create_users_table.dart │ │ └── 2024_04_21_003650_create_posts_table.dart │ ├── lib │ ├── db_config.dart │ ├── src │ │ └── models.dart │ └── test_data.dart │ ├── melos_yaroorm_test.iml │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test │ ├── integration │ ├── e2e_basic.dart │ ├── e2e_relation.dart │ ├── mariadb.e2e.dart │ ├── mysql.e2e.dart │ ├── pgsql.e2e.dart │ └── sqlite.e2e.dart │ ├── util.dart │ └── yaroorm_test.dart ├── pubspec.lock └── pubspec.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **Code Snippet** 13 | 14 | ```dart 15 | void main() { 16 | 17 | /// your code goes here 18 | 19 | } 20 | ``` 21 | 22 | **Expected Behavior** 23 | 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Additional Context** 27 | 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A new feature to be added to the project 4 | title: "feat: " 5 | labels: feature 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to add. The more context the better. 11 | 12 | **Requirements** 13 | 14 | - [ ] Checklist of requirements to be fulfilled 15 | 16 | **Additional Context** 17 | 18 | Add any other context or screenshots about the feature request go here. 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Type of Change 14 | 15 | 16 | 17 | - [ ] ✨ New feature (non-breaking change which adds functionality) 18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] 🧹 Code refactor 21 | - [ ] ✅ Build configuration change 22 | - [ ] 📝 Documentation 23 | - [ ] 🗑️ Chore 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Test Pipeline 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze Code 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v3 21 | 22 | - uses: dart-lang/setup-dart@v1.3 23 | with: 24 | sdk: "3.5.3" 25 | - uses: bluefireteam/melos-action@v3 26 | 27 | 28 | - name: Bootstrap 29 | run: | 30 | dart pub global activate melos 31 | melos bootstrap 32 | 33 | - name: Check formatting 34 | run: dart format . --line-length=120 --set-exit-if-changed 35 | 36 | - name: Check linting 37 | run: dart analyze . --fatal-infos --no-fatal-warnings 38 | 39 | test: 40 | name: Test Packages 41 | runs-on: ubuntu-latest 42 | services: 43 | mariadb: 44 | image: mariadb 45 | env: 46 | MARIADB_DATABASE: test_db 47 | MARIADB_USER: tester 48 | MARIADB_PASSWORD: password 49 | MARIADB_ROOT_PASSWORD: password 50 | ports: 51 | - 3000:3306 52 | mysqldb: 53 | image: mysql 54 | env: 55 | MYSQL_USER: 'tester' 56 | MYSQL_DATABASE: test_db 57 | MYSQL_PASSWORD: 'password' 58 | MYSQL_ROOT_PASSWORD: 'password' 59 | ports: 60 | - 3001:3306 61 | postgresdb: 62 | image: postgres:latest 63 | env: 64 | POSTGRES_DB: test_db 65 | POSTGRES_PASSWORD: password 66 | POSTGRES_USER: tester 67 | ports: 68 | - 3002:5432 69 | 70 | steps: 71 | - name: Checkout Repository 72 | uses: actions/checkout@v3 73 | 74 | - uses: dart-lang/setup-dart@v1.3 75 | with: 76 | sdk: "3.5.3" 77 | - uses: bluefireteam/melos-action@v3 78 | 79 | - name: Prepare Workspace 80 | run: | 81 | dart pub global activate melos 82 | melos bootstrap 83 | cd packages/yaroorm_test && dart run build_runner build --delete-conflicting-outputs 84 | dart run yaroorm_cli init 85 | 86 | - name: Run Unit Tests 87 | run: | 88 | cd packages/yaroorm_test 89 | dart pub global activate coverage 90 | dart pub global run coverage:test_with_coverage 91 | 92 | - name: Run E2E Tests 93 | run: ./e2e_test.sh 94 | 95 | - name: Combine Coverage 96 | run: | 97 | cd packages/yaroorm_test 98 | 99 | dart pub global run coverage:format_coverage --check-ignore --report-on=lib --lcov -o "coverage/yaroorm_lcov.info" -i ./coverage 100 | rm -rf coverage 101 | 102 | - name: Upload Coverage 103 | uses: codecov/codecov-action@v3 104 | env: 105 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 106 | with: 107 | files: packages/yaroorm_test/coverage/*_lcov.info 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | *.sqlite 9 | *.entity.dart 10 | *.g.dart 11 | !migration.g.dart 12 | .idea/ -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | packages/** 2 | docs/** 3 | docs.json 4 | docker-compose.yaml 5 | e2e_test.sh -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.lineLength": 120, 3 | "editor.formatOnSave": false 4 | } 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Yaroo project. Names should be added to the list like so: 3 | # 4 | # Name/Organization (username) - Role 5 | 6 | Chima Precious (codekeyz) - Creator & Lead Developer 7 | Amos Godwin (donkizzy) - Developer 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.4 2 | 3 | - Make Entity a mixin. 4 | - Fixed orderBy query. 5 | - Minor improvements. 6 | 7 | ## 0.0.3 8 | 9 | - Separate code-gen package from yaroorm. 10 | - Minor improvements 11 | 12 | ## 0.0.2 13 | 14 | - Added aggregate functions support. 15 | 16 | ## 0.0.1 17 | 18 | - Initial version. 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Yaroo 2 | 3 | ## Table of contents 4 | 5 | - [Get Started!](#get-started) 6 | - [Coding Guidelines](#coding-guidelines) 7 | - [Reporting an Issue](#reporting-an-issue) 8 | - [PR and Code Contributions](#PRs-and-Code-contributions) 9 | 10 | ## Get Started! 11 | 12 | ready to contribute ... 👋🏽 Let's go 🚀 13 | 14 | ### Steps for contributing 15 | 16 | 1. [Open an issue](https://github.com/yaroo/issues/new/choose) for the bug you want to fix or the 17 | feature that you want to add. 18 | 19 | 2. Fork the repo to your GitHub Account, then clone the code to your local machine. If you are not sure how to do this, 20 | GitHub's [Fork a repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo) documentation has a great step 21 | by step guide for that. 22 | 23 | 3. Set up the workspace by running the following commands 24 | 25 | ```shell 26 | dart pub global activate melos 27 | ``` 28 | 29 | and this 30 | 31 | ```shell 32 | melos bootstrap 33 | ``` 34 | 35 | ## Coding Guidelines 36 | 37 | It's good practice to create a branch for each new issue you work on, although not compulsory. 38 | 39 | - Format your code & commit the changes if any 40 | 41 | ```shell 42 | melos run format 43 | ``` 44 | 45 | - Ensure your code is properly linted by running 46 | 47 | ```shell 48 | melos run analyze 49 | ``` 50 | 51 | - Write tests for your bug-fix/feature and all tests must pass. You can verify by running 52 | 53 | ```shell 54 | melos run tests 55 | ``` 56 | 57 | If the tests pass, you can commit your changes to your fork and then create 58 | a pull request from there. Make sure to reference your issue from the pull request comments by including the issue 59 | number e.g. Resolves: #123. 60 | 61 | ### Branches 62 | 63 | Use the main branch for bug fixes or minor work that is intended for the 64 | current release stream. 65 | 66 | Use the correspondingly named branch, e.g. 2.0, for anything intended for 67 | a future release of Yaroo. 68 | 69 | ## Reporting an Issue 70 | 71 | We will typically close any vague issues or questions that are specific to some 72 | app you are writing. Please double check the docs and other references before reporting an issue or posting a question. 73 | 74 | Things that will help get your issue looked at: 75 | 76 | - Full and runnable Dart code. 77 | 78 | - Clear description of the problem or unexpected behavior. 79 | 80 | - Clear description of the expected result. 81 | 82 | - Steps you have taken to debug it yourself. 83 | 84 | - If you post a question and do not outline the above items or make it easy for us to understand and reproduce your 85 | issue, it will be closed. 86 | 87 | ## PRs and Code contributions 88 | 89 | When you've got your contribution working, all test and lint style passed, and committed to your branch it's time to 90 | create a Pull Request (PR). If you are unsure how to do this 91 | GitHub's [Creating a pull request from a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) 92 | documentation will help you with that. Once you create your PR you will be presented with a template in the PR's 93 | description that looks like this: 94 | 95 | ```md 96 | 103 | 104 | ## Description 105 | 106 | 108 | 109 | ## Type of Change 110 | 111 | 112 | 113 | - [ ] ✨ New feature (non-breaking change which adds functionality) 114 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 115 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 116 | - [ ] 🧹 Code refactor 117 | - [ ] ✅ Build configuration change 118 | - [ ] 📝 Documentation 119 | - [ ] 🗑️ Chore 120 | 121 | All you need to do is fill in the information as requested by the template. Please do not remove this as it helps both 122 | you and the reviewers confirm that the various tasks have been completed. 123 | ``` 124 | 125 | Here is an examples of good PR descriptions: 126 | 127 | - 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Yaroo Authors. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yaroorm 📦 2 | 3 | Yaroorm makes it easy to interact with databases. Currently, it provides official support for the following four databases: 4 | 5 | - SQLite 6 | - MariaDB 7 | - MySQL 8 | - PostgreSQL 9 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | # Uncomment the following section to specify additional rules. 4 | 5 | # linter: 6 | # rules: 7 | # - camel_case_types 8 | 9 | analyzer: 10 | exclude: 11 | - "**.reflectable.dart" 12 | - packages/yaroorm_test 13 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: orm-docker 2 | version: "3.8" 3 | 4 | services: 5 | mariadb: 6 | container_name: mariadb 7 | image: mariadb:latest 8 | environment: 9 | MARIADB_DATABASE: test_db 10 | MARIADB_USER: tester 11 | MARIADB_PASSWORD: password 12 | MARIADB_ROOT_PASSWORD: password 13 | ports: 14 | - "3000:3306" 15 | volumes: 16 | - mariadb_data:/var/lib/mariadb/data 17 | mysqldb: 18 | image: mysql:latest 19 | environment: 20 | MYSQL_USER: 'tester' 21 | MYSQL_PASSWORD: 'password' 22 | MYSQL_DATABASE: test_db 23 | MYSQL_ROOT_PASSWORD: password 24 | ports: 25 | - "3001:3306" 26 | volumes: 27 | - mysqldb_data:/var/lib/mysql_random/data 28 | postgresdb: 29 | image: postgres:latest 30 | environment: 31 | POSTGRES_USER: 'tester' 32 | POSTGRES_PASSWORD: 'password' 33 | POSTGRES_DB: test_db 34 | ports: 35 | - "3002:5432" 36 | volumes: 37 | - postgresdb_data:/var/lib/psql/data 38 | 39 | 40 | volumes: 41 | mariadb_data: 42 | mysqldb_data: 43 | postgresdb_data: -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Yaroo", 3 | "logo": "/assets/logo.png", 4 | "logoDark": "/assets/logo.png", 5 | "twitter": "codekeyz", 6 | "theme": "#1c2834", 7 | "favicon": "/assets/logo.png", 8 | "googleAnalytics": "", 9 | "sidebar": [ 10 | ["Overview", "/"], 11 | ["Getting Started", "/getting-started"], 12 | [ 13 | "The Basics", 14 | [ 15 | ["Routing", "/core/routing"], 16 | ["HTTP Middleware", "/core/middleware"], 17 | ["Controllers", "/core/controllers"], 18 | ["Request", "/core/request"], 19 | ["Response", "/core/response"], 20 | ["Validation", "/core/validation"] 21 | ] 22 | ], 23 | [ 24 | "ORM", 25 | [ 26 | ["Quickstart", "/orm/quickstart"], 27 | ["Relationships", "/orm/relationships"], 28 | ["Migrations", "/orm/migrations"] 29 | ] 30 | ] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /docs/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/assets/.DS_Store -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/core/controllers.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/core/controllers.mdx -------------------------------------------------------------------------------- /docs/core/middlewares.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/core/middlewares.mdx -------------------------------------------------------------------------------- /docs/core/req-res-next.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/core/req-res-next.mdx -------------------------------------------------------------------------------- /docs/core/service-providers.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/core/service-providers.mdx -------------------------------------------------------------------------------- /docs/core/validation.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/core/validation.mdx -------------------------------------------------------------------------------- /docs/getting-started.mdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/yaroorm/5c543571adb8689756cc1d7154c2bf2f7fe0e820/docs/getting-started.mdx -------------------------------------------------------------------------------- /docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Yaroo Docs 3 | description: A backend framework with just enough batteries. 4 | --- 5 | 6 | Yaroo 12 | 13 | ## About 14 | 15 | Yaroo is a modern [Server-Side](https://en.wikipedia.org/wiki/Server-side) framework written 16 | on-top of [Pharaoh](https://pub.dev/packages/pharaoh) to make backend development in Dart 17 | easy and peasy. It is very well in active development and is in use on projects such as 18 | [Dart Blog](https://dart-blog.onrender.com). 19 | 20 | Yaroo draws inspiration from popular frameworks like Laravel, NestJS, and ExpressJS, providing a streamlined learning experience. 21 | This allows you to efficiently develop your ideas without requiring extensive learning curve. Yaroo fosters common building blocks including: 22 | 23 | - Middlewares. 24 | - Controllers. 25 | - Request, Response & NextFunction. 26 | - Validation using [DTOs](https://en.wikipedia.org/wiki/Data_transfer_object). 27 | - Service Providers. 28 | 29 | ## Projects using Yaroo 30 | 31 | The following projects are using Yaroo: 32 | 33 | - [codekeyz/dart-blog](https://github.com/codekeyz/dart-blog) 34 | 35 | 36 | 37 | Submit a PR if you'd like to add your project to the list. 38 | 39 | 40 | 41 | 42 | ## License 43 | 44 | See [LICENSE](https://github.com/codekeyz/yaroo/blob/main/LICENSE) for more 45 | information. 46 | -------------------------------------------------------------------------------- /docs/orm/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Yaroorm Docs 3 | description: Learn more yaroorm for database access. 4 | --- 5 | 6 | # Quickstart 7 | 8 | This package is still in active development. If you have any feedback or feature requests, write me and issue on github. 9 | 10 | Yaroorm makes it easy to interact with databases. Currently, it provides official support for the following four databases: 11 | 12 | - SQLite 13 | - MariaDB 14 | - MySQL 15 | - PostgreSQL 16 | 17 | ## Installation 18 | 19 | To get started, add `yaroorm` as a dependency and build_runner as a dev dependency: 20 | 21 | ```shell 22 | dart pub add yaroorm 23 | ``` 24 | 25 | ## Migration 26 | 27 | Migrations in `yaroorm` keep your database structure in sync effortlessly. With `yaroorm`, you get database-agnostic support for creating and tweaking tables. 28 | 29 | Add a new file `create_users_table.dart` and add this code. 30 | 31 | ```dart 32 | import 'package:yaroorm/migration.dart'; 33 | 34 | class CreateUsersTable extends Migration { 35 | @override 36 | void up(List schemas) { 37 | final userSchema = Schema.create('users', (table) { 38 | return table 39 | ..id() 40 | ..string('name') 41 | ..string('email') 42 | ..string('password') 43 | ..timestamps(); 44 | }); 45 | 46 | schemas.add(userSchema); 47 | } 48 | 49 | @override 50 | void down(List schemas) { 51 | schemas.add(Schema.dropIfExists('users')); 52 | } 53 | } 54 | ``` 55 | 56 | ## Configuration 57 | 58 | Specify database connections for `yaroorm`, multiple connections are also supported. 59 | 60 | Add a new file `orm_config.dart` and add this code. 61 | 62 | ```dart 63 | import 'package:yaroorm/yaroorm.dart'; 64 | import 'package:path/path.dart' as path; 65 | 66 | import 'create_users_table.dart'; 67 | 68 | final config = YaroormConfig( 69 | 'test_db', // default connection 70 | connections: [ 71 | DatabaseConnection( 72 | 'test_db', 73 | DatabaseDriverType.sqlite, 74 | database: path.absolute('database', 'db.sqlite'), 75 | ), 76 | //... you can have many connections here 77 | ], 78 | migrations: [CreateUsersTable()], 79 | ); 80 | ``` 81 | ### Setup Migrations Runner 82 | 83 | Add a new file `migrator.dart` and add this code. 84 | 85 | ```dart 86 | import 'package:yaroo_cli/orm/runner.dart'; 87 | import 'package:yaroorm/yaroorm.dart'; 88 | 89 | import 'orm_config.dart' as orm; 90 | 91 | import 'migrator.reflectable.dart'; 92 | 93 | void main(List args) async { 94 | initializeReflectable(); 95 | await OrmCLIRunner.start(args, orm.config); 96 | } 97 | ``` 98 | 99 | We will need to do some code generation before proceeding. Copy & run the command below in your terminal 100 | 101 | ```shell 102 | dart run build_runner build --delete-conflicting-outputs 103 | ``` 104 | 105 | Below are the migrator commands and what they do 106 | 107 | - `migrate` # execute migrations on the database 108 | - `migrate:reset` # reset all database migrations 109 | - `migrate:rollback` # rollback last migration batch 110 | - `migrate:fresh` # reset & re-run database migrations 111 | 112 | ### Add User Entity class 113 | 114 | ```dart 115 | import 'package:yaroorm/yaroorm.dart'; 116 | 117 | @EntityMeta(table: 'users', timestamps: true) 118 | class User extends Entity { 119 | String name; 120 | int age; 121 | String email; 122 | 123 | User(this.name, this.email, this.age); 124 | } 125 | ``` -------------------------------------------------------------------------------- /e2e_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # shellcheck disable=SC2156 3 | 4 | set -e 5 | 6 | cd packages/yaroorm_test 7 | 8 | TEST_DIRECTORY="test/integration" 9 | 10 | pattern="*.e2e.dart" 11 | 12 | for file in "$TEST_DIRECTORY"/$pattern; do 13 | filename=$(basename "$file") 14 | test_name="${filename%%.*}" 15 | echo "Running tests for: $test_name 📦" 16 | dart test "$file" --coverage=coverage --fail-fast 17 | done 18 | -------------------------------------------------------------------------------- /lib/src/config.dart: -------------------------------------------------------------------------------- 1 | import 'database/driver/driver.dart'; 2 | 3 | class YaroormConfig { 4 | final String defaultConnName; 5 | final List connections; 6 | 7 | final String migrationsTable; 8 | 9 | DatabaseConnection get defaultDBConn => connections.firstWhere((e) => e.name == defaultConnName); 10 | 11 | YaroormConfig( 12 | this.defaultConnName, { 13 | required this.connections, 14 | this.migrationsTable = 'migrations', 15 | }) { 16 | final hasDefault = connections.any((e) => e.name == defaultConnName); 17 | if (!hasDefault) { 18 | throw ArgumentError('Database connection info not found for $defaultConnName'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/database/database.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | import 'package:meta/meta_meta.dart'; 3 | 4 | import '../config.dart'; 5 | import '../migration.dart'; 6 | import '../query/query.dart'; 7 | import 'entity/entity.dart'; 8 | import 'driver/driver.dart'; 9 | 10 | class UseDatabaseConnection { 11 | final DatabaseConnection info; 12 | late final DatabaseDriver driver; 13 | 14 | UseDatabaseConnection(this.info) : driver = DB.driver(info.name); 15 | 16 | Query query>([String? table]) { 17 | return Query.table(table, info.database).driver(driver); 18 | } 19 | } 20 | 21 | const String pleaseInitializeMessage = 'DB has not been initialized.\n' 22 | 'Please make sure that you have called `DB.init(DatabaseConfig)`.'; 23 | 24 | @Target({TargetKind.topLevelVariable}) 25 | class UseORMConfig { 26 | const UseORMConfig._(); 27 | } 28 | 29 | class DB { 30 | static const useConfig = UseORMConfig._(); 31 | 32 | static List migrations = []; 33 | 34 | static YaroormConfig config = throw StateError(pleaseInitializeMessage); 35 | 36 | static final Map _driverInstances = {}; 37 | 38 | static late final UseDatabaseConnection defaultConnection; 39 | 40 | DB._(); 41 | 42 | static DatabaseDriver get defaultDriver => defaultConnection.driver; 43 | 44 | static Query query>([String? table]) => defaultConnection.query(table); 45 | 46 | static UseDatabaseConnection connection(String connName) => 47 | UseDatabaseConnection(config.connections.firstWhere((e) => e.name == connName)); 48 | 49 | /// This call returns the driver for a connection 50 | /// 51 | /// [connName] is the connection name you defined in [YaroormConfig] 52 | static DatabaseDriver driver(String connName) { 53 | if (connName == 'default') return defaultDriver; 54 | final instance = _driverInstances[connName]; 55 | if (instance != null) return instance; 56 | final connInfo = config.connections.firstWhereOrNull((e) => e.name == connName); 57 | if (connInfo == null) { 58 | throw ArgumentError.value( 59 | connName, 60 | 'No Database connection found with name: $connName', 61 | ); 62 | } 63 | return _driverInstances[connName] = DatabaseDriver.init(connInfo); 64 | } 65 | 66 | static void init(YaroormConfig config) { 67 | DB.config = config; 68 | 69 | final defaultConn = config.defaultDBConn; 70 | DB._driverInstances[defaultConn.name] = DatabaseDriver.init(defaultConn); 71 | DB.defaultConnection = UseDatabaseConnection(defaultConn); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/database/driver/driver.dart: -------------------------------------------------------------------------------- 1 | import 'mysql_driver.dart'; 2 | import 'pgsql_driver.dart'; 3 | 4 | import '../../primitives/serializer.dart'; 5 | import '../../query/query.dart'; 6 | import '../../migration.dart'; 7 | 8 | import '../entity/entity.dart'; 9 | import 'sqlite_driver.dart'; 10 | 11 | enum DatabaseDriverType { sqlite, pgsql, mysql, mariadb } 12 | 13 | String wrapString(String value) => "'$value'"; 14 | 15 | class DatabaseConnection { 16 | final String name; 17 | final String? url; 18 | final String? host; 19 | final int? port; 20 | final String? username; 21 | final String? password; 22 | final String database; 23 | final String? charset, collation; 24 | final bool dbForeignKeys; 25 | final DatabaseDriverType driver; 26 | final bool? secure; 27 | final String timeZone; 28 | 29 | const DatabaseConnection( 30 | this.name, 31 | this.driver, { 32 | required this.database, 33 | this.charset, 34 | this.collation, 35 | this.host, 36 | this.password, 37 | this.port, 38 | this.url, 39 | this.username, 40 | this.dbForeignKeys = true, 41 | this.secure, 42 | this.timeZone = 'UTC', 43 | }); 44 | 45 | factory DatabaseConnection.fromJson(Map connInfo) { 46 | return DatabaseConnection( 47 | connInfo['name'], 48 | getDriverType(connInfo['driver']), 49 | database: connInfo['database'], 50 | host: connInfo['host'], 51 | port: connInfo['port'] == null ? null : int.tryParse(connInfo['port']), 52 | charset: connInfo['charset'], 53 | collation: connInfo['collation'], 54 | password: connInfo['password'], 55 | username: connInfo['username'], 56 | url: connInfo['url'], 57 | secure: connInfo['secure'], 58 | dbForeignKeys: connInfo['foreign_key_constraints'] ?? true, 59 | timeZone: connInfo['timezone'] ?? 'UTC', 60 | ); 61 | } 62 | 63 | static DatabaseDriverType getDriverType(String driver) { 64 | return switch (driver) { 65 | 'sqlite' => DatabaseDriverType.sqlite, 66 | 'pgsql' => DatabaseDriverType.pgsql, 67 | 'mysql' => DatabaseDriverType.mysql, 68 | 'mariadb' => DatabaseDriverType.mariadb, 69 | _ => throw ArgumentError.value(driver, null, 'Invalid Database Driver provided in configuration') 70 | }; 71 | } 72 | } 73 | 74 | mixin DriverContract { 75 | /// Perform query on the database 76 | Future>> query(ReadQuery query); 77 | 78 | /// Perform raw query on the database. 79 | Future>> rawQuery(String script); 80 | 81 | /// Execute scripts on the database. 82 | /// 83 | /// Execution varies across drivers 84 | Future execute(String script); 85 | 86 | /// Perform update on the database 87 | Future update(UpdateQuery query); 88 | 89 | /// Perform delete on the database 90 | Future delete(DeleteQuery query); 91 | 92 | /// Perform insert on the database 93 | Future insert(InsertQuery query); 94 | 95 | /// Perform insert on the database 96 | Future insertMany(InsertManyQuery query); 97 | 98 | PrimitiveSerializer get serializer; 99 | 100 | List get typeconverters => []; 101 | 102 | DatabaseDriverType get type; 103 | } 104 | 105 | abstract class DriverTransactor with DriverContract {} 106 | 107 | abstract interface class DatabaseDriver with DriverContract { 108 | factory DatabaseDriver.init(DatabaseConnection dbConn) { 109 | final driver = dbConn.driver; 110 | switch (driver) { 111 | case DatabaseDriverType.sqlite: 112 | return SqliteDriver(dbConn); 113 | case DatabaseDriverType.mariadb: 114 | case DatabaseDriverType.mysql: 115 | return MySqlDriver(dbConn, driver); 116 | case DatabaseDriverType.pgsql: 117 | return PostgreSqlDriver(dbConn); 118 | } 119 | } 120 | 121 | /// Check if the database is open for operation 122 | bool get isOpen; 123 | 124 | Future connect({int? maxConnections, bool? singleConnection}); 125 | 126 | Future disconnect(); 127 | 128 | /// check if the table exists in the database 129 | Future hasTable(String tableName); 130 | 131 | TableBlueprint get blueprint; 132 | 133 | Future transaction( 134 | void Function(DriverTransactor transactor) transaction, 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /lib/src/database/driver/mysql_driver.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:mysql_client/mysql_client.dart'; 3 | import '../../migration.dart'; 4 | import '../entity/entity.dart'; 5 | import '../../query/query.dart'; 6 | 7 | import '../../primitives/serializer.dart'; 8 | import '../entity/misc.dart'; 9 | import 'driver.dart'; 10 | import 'sqlite_driver.dart'; 11 | 12 | final _serializer = MySqlPrimitiveSerializer(); 13 | 14 | const _mysqlTypeConverters = [ 15 | dateTimeConverter, 16 | ...defaultListConverters, 17 | ]; 18 | 19 | final class MySqlDriver implements DatabaseDriver { 20 | final DatabaseConnection config; 21 | final DatabaseDriverType _type; 22 | 23 | late MySQLConnection _dbConnection; 24 | 25 | int get portToUse => config.port ?? 3306; 26 | 27 | MySqlDriver(this.config, this._type) { 28 | assert([DatabaseDriverType.mysql, DatabaseDriverType.mariadb].contains(config.driver)); 29 | assert(config.host != null, 'Host is required'); 30 | } 31 | 32 | @override 33 | Future connect({int? maxConnections, bool? singleConnection}) async { 34 | assert(maxConnections == null, '${_type.name} max connections not yet supported'); 35 | final secure = config.secure ?? false; 36 | 37 | if (secure) { 38 | assert(config.username != null, 'Username is required when :secure true'); 39 | assert(config.password != null, 'Password is required when :secure true'); 40 | } 41 | 42 | _dbConnection = await MySQLConnection.createConnection( 43 | host: config.host!, 44 | port: portToUse, 45 | secure: secure, 46 | userName: config.username ?? '', 47 | password: config.password ?? '', 48 | databaseName: config.database, 49 | ); 50 | await _dbConnection.connect(); 51 | return this; 52 | } 53 | 54 | @override 55 | Future disconnect() async { 56 | if (!_dbConnection.connected) return; 57 | return _dbConnection.close(); 58 | } 59 | 60 | @override 61 | bool get isOpen => _dbConnection.connected; 62 | 63 | @override 64 | Future>> rawQuery(String script) async { 65 | if (!isOpen) await connect(); 66 | final result = await _dbConnection.execute(script); 67 | return result.rows.map((e) => e.typedAssoc()).toList(); 68 | } 69 | 70 | @override 71 | Future execute(String script) => rawQuery(script); 72 | 73 | @override 74 | Future>> query(ReadQuery query) { 75 | final sql = _serializer.acceptReadQuery(query); 76 | return rawQuery(sql); 77 | } 78 | 79 | @override 80 | Future>> delete(DeleteQuery query) async { 81 | final sql = _serializer.acceptDeleteQuery(query); 82 | return rawQuery(sql); 83 | } 84 | 85 | @override 86 | Future>> update(UpdateQuery query) async { 87 | final result = await _dbConnection.execute(_serializer.acceptUpdateQuery(query), query.data); 88 | return result.rows.map((e) => e.typedAssoc()).toList(); 89 | } 90 | 91 | @override 92 | Future insert(InsertQuery query) async { 93 | final result = await _dbConnection.execute(_serializer.acceptInsertQuery(query), query.data); 94 | return result.lastInsertID.toInt(); 95 | } 96 | 97 | @override 98 | Future insertMany(InsertManyQuery query) async { 99 | return await _dbConnection.transactional((conn) => _MysqlTransactor(conn, type).insertMany(query)); 100 | } 101 | 102 | @override 103 | Future hasTable(String tableName) async { 104 | final sql = 105 | 'SELECT 1 FROM information_schema.tables WHERE table_schema = ${wrapString(config.database)} AND table_name = ${wrapString(tableName)} LIMIT 1'; 106 | final result = await rawQuery(sql); 107 | return result.isNotEmpty; 108 | } 109 | 110 | @override 111 | Future transaction(void Function(DriverTransactor transactor) func) => 112 | _dbConnection.transactional((txn) => func(_MysqlTransactor(txn, type))); 113 | 114 | @override 115 | DatabaseDriverType get type => _type; 116 | 117 | @override 118 | TableBlueprint get blueprint => MySqlDriverTableBlueprint(); 119 | 120 | @override 121 | PrimitiveSerializer get serializer => _serializer; 122 | 123 | @override 124 | List get typeconverters => _mysqlTypeConverters; 125 | } 126 | 127 | class _MysqlTransactor extends DriverTransactor { 128 | final MySQLConnection _dbConn; 129 | 130 | @override 131 | final DatabaseDriverType type; 132 | 133 | _MysqlTransactor(this._dbConn, this.type); 134 | 135 | @override 136 | Future execute(String script) => rawQuery(script); 137 | 138 | @override 139 | Future>> rawQuery(String script) async { 140 | final rows = (await _dbConn.execute(script)).rows; 141 | if (rows.isEmpty) return []; 142 | return rows.map((e) => e.typedAssoc()).toList(); 143 | } 144 | 145 | @override 146 | Future>> query(ReadQuery query) { 147 | final sql = _serializer.acceptReadQuery(query); 148 | return rawQuery(sql); 149 | } 150 | 151 | @override 152 | Future delete(DeleteQuery query) async { 153 | final sql = _serializer.acceptDeleteQuery(query); 154 | await rawQuery(sql); 155 | } 156 | 157 | @override 158 | Future update(UpdateQuery query) async { 159 | await _dbConn.execute(_serializer.acceptUpdateQuery(query), query.data); 160 | } 161 | 162 | @override 163 | Future insert(InsertQuery query) async { 164 | final result = await _dbConn.execute(_serializer.acceptInsertQuery(query), query.data); 165 | return result.lastInsertID.toInt(); 166 | } 167 | 168 | @override 169 | Future insertMany(InsertManyQuery query) async { 170 | final sql = _serializer.acceptInsertManyQuery(query); 171 | for (final value in query.values) { 172 | await _dbConn.execute(sql, value); 173 | } 174 | } 175 | 176 | @override 177 | PrimitiveSerializer get serializer => _serializer; 178 | 179 | @override 180 | List get typeconverters => _mysqlTypeConverters; 181 | } 182 | 183 | @protected 184 | class MySqlDriverTableBlueprint extends SqliteTableBlueprint { 185 | @override 186 | PrimitiveSerializer get szler => _serializer; 187 | 188 | @override 189 | void id({String name = 'id', String? type, bool autoIncrement = true}) { 190 | type ??= 'INT'; 191 | final sb = StringBuffer()..write('${_serializer.escapeStr(name)} $type NOT NULL PRIMARY KEY'); 192 | if (autoIncrement) sb.write(' AUTO_INCREMENT'); 193 | statements.add(sb.toString()); 194 | } 195 | 196 | @override 197 | void string( 198 | String name, { 199 | bool nullable = false, 200 | String? defaultValue, 201 | int length = 255, 202 | bool unique = false, 203 | }) { 204 | statements.add(makeColumn( 205 | name, 206 | 'VARCHAR($length)', 207 | nullable: nullable, 208 | defaultValue: defaultValue, 209 | unique: unique, 210 | )); 211 | } 212 | 213 | @override 214 | void datetime( 215 | String name, { 216 | bool nullable = false, 217 | DateTime? defaultValue, 218 | bool unique = false, 219 | }) { 220 | statements.add(makeColumn(name, 'DATETIME', nullable: nullable, defaultValue: defaultValue, unique: unique)); 221 | } 222 | 223 | @override 224 | void timestamp(String name, {bool nullable = false, DateTime? defaultValue, bool unique = false}) { 225 | statements.add(makeColumn(name, 'TIMESTAMP', nullable: nullable, defaultValue: defaultValue, unique: unique)); 226 | } 227 | 228 | @override 229 | void date(String name, {bool nullable = false, DateTime? defaultValue, unique = false}) { 230 | statements.add(makeColumn(name, 'DATE', nullable: nullable, defaultValue: defaultValue, unique: unique)); 231 | } 232 | 233 | @override 234 | void time(String name, {bool nullable = false, DateTime? defaultValue, unique = false}) { 235 | statements.add(makeColumn(name, 'TIME', nullable: nullable, defaultValue: defaultValue, unique: unique)); 236 | } 237 | 238 | @override 239 | void boolean(String name, {bool nullable = false, bool? defaultValue, unique = false}) { 240 | statements.add(makeColumn(name, 'BOOLEAN', nullable: nullable, defaultValue: defaultValue, unique: unique)); 241 | } 242 | 243 | /// NUMERIC TYPES 244 | /// ---------------------------------------------------------------- 245 | 246 | @override 247 | void float(String name, {bool nullable = false, num? defaultValue, int? precision, int? scale, unique = false}) { 248 | final type = 'FLOAT(${precision ?? 10}, ${scale ?? 0})'; 249 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 250 | } 251 | 252 | @override 253 | void double(String name, {bool nullable = false, num? defaultValue, int? precision, int? scale, unique = false}) { 254 | final type = 'DOUBLE(${precision ?? 10}, ${scale ?? 0})'; 255 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 256 | } 257 | 258 | @override 259 | void tinyInt(String name, {bool nullable = false, num? defaultValue, unique = false}) { 260 | statements.add(makeColumn(name, 'TINYINT', nullable: nullable, defaultValue: defaultValue, unique: unique)); 261 | } 262 | 263 | @override 264 | void smallInteger(String name, {bool nullable = false, num? defaultValue, unique = false}) { 265 | statements.add(makeColumn(name, 'SMALLINT', nullable: nullable, defaultValue: defaultValue, unique: unique)); 266 | } 267 | 268 | @override 269 | void mediumInteger(String name, {bool nullable = false, num? defaultValue, unique = false}) { 270 | statements.add(makeColumn(name, 'MEDIUMINT', nullable: nullable, defaultValue: defaultValue, unique: unique)); 271 | } 272 | 273 | @override 274 | void bigInteger(String name, {bool nullable = false, num? defaultValue, unique = false}) { 275 | statements.add(makeColumn(name, 'BIGINT', nullable: nullable, defaultValue: defaultValue, unique: unique)); 276 | } 277 | 278 | @override 279 | void decimal( 280 | String name, { 281 | bool nullable = false, 282 | num? defaultValue, 283 | int? precision, 284 | int? scale, 285 | unique = false, 286 | }) { 287 | final type = 'DECIMAL(${precision ?? 10}, ${scale ?? 0})'; 288 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 289 | } 290 | 291 | @override 292 | void numeric( 293 | String name, { 294 | bool nullable = false, 295 | num? defaultValue, 296 | int? precision, 297 | int? scale, 298 | unique = false, 299 | }) { 300 | final type = 'NUMERIC(${precision ?? 10}, ${scale ?? 0})'; 301 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 302 | } 303 | 304 | @override 305 | void bit(String name, {bool nullable = false, int? defaultValue, unique = false}) { 306 | statements.add(makeColumn(name, 'BIT', nullable: nullable, defaultValue: defaultValue, unique: unique)); 307 | } 308 | 309 | /// STRING TYPES 310 | /// ---------------------------------------------------------------- 311 | 312 | String _getStringType(String type, {String? charset, String? collate}) { 313 | final sb = StringBuffer()..write(type); 314 | if (charset != null) sb.write(' CHARACTER SET $charset'); 315 | if (collate != null) sb.write(' COLLATE $collate'); 316 | return sb.toString(); 317 | } 318 | 319 | /// BLOB typs cannot have default values see here: https://dev.mysql.com/doc/refman/8.0/en/blob.html 320 | @override 321 | void blob(String name, {bool nullable = false, defaultValue}) { 322 | final type = _getStringType('BLOB'); 323 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: null)); 324 | } 325 | 326 | /// TEXT type cannot have default values see here: https://dev.mysql.com/doc/refman/8.0/en/blob.html 327 | @override 328 | void text( 329 | String name, { 330 | bool nullable = false, 331 | String? defaultValue, 332 | String? charset, 333 | String? collate, 334 | int length = 1, 335 | unique = false, 336 | }) { 337 | final type = _getStringType('TEXT($length)', charset: charset, collate: collate); 338 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: null)); 339 | } 340 | 341 | @override 342 | void longText( 343 | String name, { 344 | bool nullable = false, 345 | String? defaultValue, 346 | String? charset, 347 | String? collate, 348 | unique = false, 349 | }) { 350 | final type = _getStringType('LONGTEXT', charset: charset, collate: collate); 351 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 352 | } 353 | 354 | @override 355 | void mediumText( 356 | String name, { 357 | bool nullable = false, 358 | String? defaultValue, 359 | String? charset, 360 | String? collate, 361 | unique = false, 362 | }) { 363 | final type = _getStringType('MEDIUMTEXT', charset: charset, collate: collate); 364 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 365 | } 366 | 367 | @override 368 | void tinyText( 369 | String name, { 370 | bool nullable = false, 371 | String? defaultValue, 372 | String? charset, 373 | String? collate, 374 | unique = false, 375 | }) { 376 | final type = _getStringType('TINYTEXT', charset: charset, collate: collate); 377 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 378 | } 379 | 380 | @override 381 | void char( 382 | String name, { 383 | bool nullable = false, 384 | String? defaultValue, 385 | String? charset, 386 | String? collate, 387 | int length = 1, 388 | unique = false, 389 | }) { 390 | final type = _getStringType('CHAR($length)', charset: charset, collate: collate); 391 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 392 | } 393 | 394 | @override 395 | void varchar( 396 | String name, { 397 | bool nullable = false, 398 | String? defaultValue, 399 | int length = 255, 400 | String? charset, 401 | String? collate, 402 | unique = false, 403 | }) { 404 | final type = _getStringType('VARCHAR($length)', charset: charset, collate: collate); 405 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 406 | } 407 | 408 | @override 409 | void enums( 410 | String name, 411 | List values, { 412 | bool nullable = false, 413 | String? defaultValue, 414 | String? charset, 415 | String? collate, 416 | unique = false, 417 | }) { 418 | final type = _getStringType('ENUM(${values.join(', ')})', charset: charset, collate: collate); 419 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 420 | } 421 | 422 | @override 423 | void set( 424 | String name, 425 | List values, { 426 | bool nullable = false, 427 | String? defaultValue, 428 | String? charset, 429 | String? collate, 430 | unique = false, 431 | }) { 432 | final type = _getStringType('SET(${values.join(', ')})', charset: charset, collate: collate); 433 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue, unique: unique)); 434 | } 435 | 436 | @override 437 | void binary(String name, 438 | {bool nullable = false, String? defaultValue, String? charset, String? collate, int size = 1}) { 439 | final type = _getStringType('BINARY($size)', charset: charset, collate: collate); 440 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue)); 441 | } 442 | 443 | @override 444 | void varbinary(String name, 445 | {bool nullable = false, String? defaultValue, String? charset, String? collate, int size = 1}) { 446 | final type = _getStringType('VARBINARY($size)', charset: charset, collate: collate); 447 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue)); 448 | } 449 | } 450 | 451 | @protected 452 | class MySqlPrimitiveSerializer extends SqliteSerializer { 453 | const MySqlPrimitiveSerializer(); 454 | 455 | @override 456 | String acceptInsertQuery(InsertQuery query) { 457 | final keys = query.data.keys; 458 | final parameters = keys.map((e) => ':$e').join(', '); 459 | return 'INSERT INTO ${query.tableName} (${keys.join(', ')}) VALUES ($parameters)$terminator'; 460 | } 461 | 462 | @override 463 | String acceptInsertManyQuery(InsertManyQuery query) { 464 | final keys = query.values.first.keys; 465 | final parameters = keys.map((e) => ':$e').join(', '); 466 | return 'INSERT INTO ${query.tableName} (${keys.join(', ')}) VALUES ($parameters)$terminator'; 467 | } 468 | 469 | @override 470 | String acceptUpdateQuery(UpdateQuery query) { 471 | final queryBuilder = StringBuffer(); 472 | 473 | final fields = query.data.keys.map((e) => '${escapeStr(e)} = :$e').join(', '); 474 | 475 | queryBuilder.write('UPDATE ${escapeStr(query.tableName)}'); 476 | 477 | queryBuilder 478 | ..write(' SET $fields') 479 | ..write(' WHERE ${acceptWhereClause(query.whereClause)}') 480 | ..write(terminator); 481 | 482 | return queryBuilder.toString(); 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /lib/src/database/driver/pgsql_driver.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:postgres/postgres.dart' as pg; 3 | 4 | import '../../migration.dart'; 5 | import '../../primitives/serializer.dart'; 6 | import '../../query/query.dart'; 7 | import '../entity/entity.dart'; 8 | import '../entity/misc.dart'; 9 | import 'driver.dart'; 10 | import 'mysql_driver.dart'; 11 | 12 | final _pgsqlSerializer = PgSqlPrimitiveSerializer(); 13 | 14 | final class PostgreSqlDriver implements DatabaseDriver { 15 | final DatabaseConnection config; 16 | pg.Connection? db; 17 | 18 | PostgreSqlDriver(this.config); 19 | 20 | @override 21 | Future connect({int? maxConnections, bool? singleConnection, bool? secure}) async { 22 | assert(maxConnections == null, 'Postgres max connections not supported'); 23 | secure ??= false; 24 | 25 | if (secure) { 26 | assert(config.username != null, 'Username is required when :secure true'); 27 | assert(config.password != null, 'Password is required when :secure true'); 28 | } 29 | 30 | db = await pg.Connection.open( 31 | pg.Endpoint( 32 | host: config.host!, 33 | database: config.database, 34 | username: config.username, 35 | password: config.password, 36 | port: config.port == null ? 5432 : config.port!, 37 | ), 38 | settings: pg.ConnectionSettings( 39 | sslMode: secure ? pg.SslMode.require : pg.SslMode.disable, 40 | timeZone: config.timeZone, 41 | ), 42 | ); 43 | return this; 44 | } 45 | 46 | @override 47 | Future>> delete(DeleteQuery query) { 48 | final sqlScript = serializer.acceptDeleteQuery(query); 49 | return _execRawQuery(sqlScript); 50 | } 51 | 52 | @override 53 | Future disconnect() async { 54 | if (!isOpen) return; 55 | await db?.close(); 56 | } 57 | 58 | Future>> _execRawQuery(String script, {Map? parameters}) async { 59 | parameters ??= {}; 60 | if (!isOpen) await connect(); 61 | final result = await db!.execute(pg.Sql.named(script), parameters: parameters); 62 | return result.map((e) => e.toColumnMap()).toList(); 63 | } 64 | 65 | @override 66 | Future execute(String script) => rawQuery(script); 67 | 68 | @override 69 | Future insert(InsertQuery query) async { 70 | if (!isOpen) await connect(); 71 | final values = {...query.data}; 72 | final sql = _pgsqlSerializer.acceptInsertQuery(query); 73 | final result = await db!.execute(pg.Sql.named(sql), parameters: values); 74 | return result[0][0]; 75 | } 76 | 77 | @override 78 | bool get isOpen => db?.isOpen ?? false; 79 | 80 | @override 81 | Future>> query(ReadQuery query) async { 82 | final sqlScript = serializer.acceptReadQuery(query); 83 | return _execRawQuery(sqlScript); 84 | } 85 | 86 | @override 87 | Future>> update(UpdateQuery query) { 88 | return _execRawQuery(serializer.acceptUpdateQuery(query), parameters: query.data); 89 | } 90 | 91 | @override 92 | PrimitiveSerializer get serializer => _pgsqlSerializer; 93 | 94 | @override 95 | DatabaseDriverType get type => DatabaseDriverType.pgsql; 96 | 97 | @override 98 | TableBlueprint get blueprint => PgSqlTableBlueprint(); 99 | 100 | @override 101 | Future hasTable(String tableName) async { 102 | final result = await _execRawQuery( 103 | '''SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' AND table_name=@tableName;''', 104 | parameters: {'tableName': tableName}); 105 | if (result.isEmpty) return false; 106 | return true; 107 | } 108 | 109 | @override 110 | Future>> rawQuery(String script) => _execRawQuery(script); 111 | 112 | @override 113 | Future transaction(void Function(DriverTransactor transactor) func) async { 114 | if (!isOpen) await connect(); 115 | if (db == null) return Future.value(); 116 | return db!.runTx((txn) async => func(_PgSqlDriverTransactor(txn))); 117 | } 118 | 119 | @override 120 | Future insertMany(InsertManyQuery query) async { 121 | if (!isOpen) await connect(); 122 | final sql = _pgsqlSerializer.acceptInsertManyQuery(query); 123 | final result = await db?.execute(sql); 124 | return result?.expand((x) => x).toList(); 125 | } 126 | 127 | @override 128 | List get typeconverters => [ 129 | booleanConverter, 130 | ...defaultListConverters, 131 | ]; 132 | } 133 | 134 | class _PgSqlDriverTransactor extends DriverTransactor { 135 | final pg.TxSession txn; 136 | 137 | _PgSqlDriverTransactor(this.txn); 138 | 139 | @override 140 | DatabaseDriverType get type => DatabaseDriverType.pgsql; 141 | 142 | @override 143 | Future delete(DeleteQuery query) async { 144 | final sql = _pgsqlSerializer.acceptDeleteQuery(query); 145 | await rawQuery(sql); 146 | } 147 | 148 | @override 149 | Future execute(String script) => rawQuery(script); 150 | 151 | @override 152 | Future insert(InsertQuery query) async { 153 | final sql = _pgsqlSerializer.acceptInsertQuery(query); 154 | final result = await txn.execute(pg.Sql.named(sql), parameters: query.data); 155 | return result.first.toColumnMap()[query.primaryKey]; 156 | } 157 | 158 | @override 159 | Future insertMany(InsertManyQuery query) { 160 | final sql = _pgsqlSerializer.acceptInsertManyQuery(query); 161 | return rawQuery(sql); 162 | } 163 | 164 | @override 165 | Future>> query(ReadQuery query) { 166 | final sql = _pgsqlSerializer.acceptReadQuery(query); 167 | return rawQuery(sql); 168 | } 169 | 170 | @override 171 | Future>> rawQuery(String script) async { 172 | final result = await txn.execute(script); 173 | return result.map((e) => e.toColumnMap()).toList(); 174 | } 175 | 176 | @override 177 | PrimitiveSerializer get serializer => _pgsqlSerializer; 178 | 179 | @override 180 | Future update(UpdateQuery query) async { 181 | final sql = _pgsqlSerializer.acceptUpdateQuery(query); 182 | await rawQuery(sql); 183 | } 184 | } 185 | 186 | @protected 187 | class PgSqlPrimitiveSerializer extends MySqlPrimitiveSerializer { 188 | const PgSqlPrimitiveSerializer(); 189 | 190 | @override 191 | String acceptInsertQuery(InsertQuery query) { 192 | final keys = query.data.keys; 193 | final parameters = keys.map((e) => '@$e').join(', '); 194 | return 'INSERT INTO ${query.tableName} (${keys.map(escapeStr).join(', ')}) VALUES ($parameters) RETURNING "${query.primaryKey}"$terminator'; 195 | } 196 | 197 | @override 198 | String acceptUpdateQuery(UpdateQuery query) { 199 | final queryBuilder = StringBuffer(); 200 | 201 | final fields = query.data.keys.map((e) => '${escapeStr(e)} = @$e').join(', '); 202 | 203 | queryBuilder.write('UPDATE ${escapeStr(query.tableName)}'); 204 | 205 | queryBuilder 206 | ..write(' SET $fields') 207 | ..write(' WHERE ${acceptWhereClause(query.whereClause)}') 208 | ..write(terminator); 209 | 210 | return queryBuilder.toString(); 211 | } 212 | 213 | @override 214 | String acceptInsertManyQuery(InsertManyQuery query) { 215 | final fields = query.values.first.keys.map(escapeStr).join(', '); 216 | final values = query.values.map((dataMap) { 217 | final values = dataMap.values.map((value) => "'$value'").join(', '); 218 | return '($values)'; 219 | }).join(', '); 220 | return 'INSERT INTO ${query.tableName} ($fields) VALUES $values RETURNING ${query.primaryKey}$terminator'; 221 | } 222 | 223 | @override 224 | String escapeStr(String column) => '"${super.escapeStr(column)}"'; 225 | } 226 | 227 | @protected 228 | class PgSqlTableBlueprint extends MySqlDriverTableBlueprint { 229 | @override 230 | PrimitiveSerializer get szler => _pgsqlSerializer; 231 | 232 | @override 233 | void id({name = 'id', String? type, autoIncrement = true}) { 234 | type ??= 'SERIAL'; 235 | 236 | final sb = StringBuffer()..write(szler.escapeStr(name)); 237 | sb.write(autoIncrement ? " SERIAL PRIMARY KEY" : " $type PRIMARY KEY"); 238 | statements.add(sb.toString()); 239 | } 240 | 241 | @override 242 | void datetime(String name, {bool nullable = false, DateTime? defaultValue, unique = false}) { 243 | statements.add(makeColumn( 244 | name, 245 | 'TIMESTAMP', 246 | nullable: nullable, 247 | defaultValue: defaultValue, 248 | unique: unique, 249 | )); 250 | } 251 | 252 | @override 253 | void blob(String name, {bool nullable = false, defaultValue}) { 254 | statements.add(makeColumn(name, "BYTEA", nullable: nullable, defaultValue: null)); 255 | } 256 | 257 | @override 258 | void boolean(String name, {nullable = false, defaultValue, unique = false}) { 259 | statements.add(makeColumn( 260 | name, 261 | 'BOOLEAN', 262 | nullable: nullable, 263 | defaultValue: defaultValue, 264 | unique: unique, 265 | )); 266 | } 267 | 268 | @override 269 | String renameScript(String fromName, String toName) { 270 | return 'ALTER TABLE "${szler.escapeStr(fromName)}" RENAME TO "${szler.escapeStr(toName)}";'; 271 | } 272 | 273 | @override 274 | void float( 275 | String name, { 276 | bool nullable = false, 277 | num? defaultValue, 278 | int? precision, 279 | int? scale, 280 | unique = false, 281 | }) { 282 | statements.add(makeColumn( 283 | name, 284 | 'DOUBLE PRECISION', 285 | nullable: nullable, 286 | defaultValue: defaultValue, 287 | unique: unique, 288 | )); 289 | } 290 | 291 | @override 292 | void double( 293 | String name, { 294 | bool nullable = false, 295 | num? defaultValue, 296 | int? precision = 10, 297 | int? scale = 0, 298 | unique = false, 299 | }) { 300 | statements.add(makeColumn( 301 | name, 302 | 'NUMERIC($precision, $scale)', 303 | nullable: nullable, 304 | defaultValue: defaultValue, 305 | unique: unique, 306 | )); 307 | } 308 | 309 | @override 310 | void tinyInt( 311 | String name, { 312 | bool nullable = false, 313 | num? defaultValue, 314 | unique = false, 315 | }) { 316 | throw UnimplementedError('tinyInt not implemented for Postgres'); 317 | } 318 | 319 | @override 320 | void mediumInteger( 321 | String name, { 322 | bool nullable = false, 323 | num? defaultValue, 324 | unique = false, 325 | }) { 326 | statements.add(makeColumn( 327 | name, 328 | 'INTEGER', 329 | nullable: nullable, 330 | defaultValue: defaultValue, 331 | unique: unique, 332 | )); 333 | } 334 | 335 | @override 336 | void text( 337 | String name, { 338 | bool nullable = false, 339 | String? defaultValue, 340 | String? charset, 341 | String? collate, 342 | int length = 1, 343 | unique = false, 344 | }) { 345 | statements.add(makeColumn( 346 | name, 347 | 'TEXT', 348 | nullable: nullable, 349 | defaultValue: null, 350 | unique: unique, 351 | )); 352 | } 353 | 354 | @override 355 | void longText( 356 | String name, { 357 | bool nullable = false, 358 | String? defaultValue, 359 | String? charset, 360 | String? collate, 361 | unique = false, 362 | }) { 363 | throw UnimplementedError('longText not implemented for Postgres'); 364 | } 365 | 366 | @override 367 | void mediumText( 368 | String name, { 369 | bool nullable = false, 370 | String? defaultValue, 371 | String? charset, 372 | String? collate, 373 | unique = false, 374 | }) { 375 | throw UnimplementedError('mediumText not implemented for Postgres'); 376 | } 377 | 378 | @override 379 | void tinyText( 380 | String name, { 381 | bool nullable = false, 382 | String? defaultValue, 383 | String? charset, 384 | String? collate, 385 | unique = false, 386 | }) { 387 | throw UnimplementedError('tinyText not implemented for Postgres'); 388 | } 389 | 390 | @override 391 | void binary( 392 | String name, { 393 | bool nullable = false, 394 | String? defaultValue, 395 | String? charset, 396 | String? collate, 397 | int size = 1, 398 | }) { 399 | statements.add(makeColumn(name, "BYTEA", nullable: nullable, defaultValue: defaultValue)); 400 | } 401 | 402 | @override 403 | void varbinary( 404 | String name, { 405 | bool nullable = false, 406 | String? defaultValue, 407 | String? charset, 408 | String? collate, 409 | int size = 1, 410 | }) { 411 | final type = 'BIT VARYING($size)'; 412 | statements.add(makeColumn(name, type, nullable: nullable, defaultValue: defaultValue)); 413 | } 414 | 415 | @override 416 | void enums( 417 | String name, 418 | List values, { 419 | bool nullable = false, 420 | String? defaultValue, 421 | String? charset, 422 | String? collate, 423 | unique = false, 424 | }) { 425 | final sb = StringBuffer() 426 | ..write( 427 | 'CREATE TYPE ${szler.escapeStr(name)} AS ENUM (${values.map((e) => "'$e'").join(', ')});', 428 | ); 429 | 430 | if (unique) { 431 | sb.write(' UNIQUE'); 432 | } 433 | 434 | if (!nullable) { 435 | sb.write(' NOT NULL'); 436 | if (defaultValue != null) sb.write(' DEFAULT $defaultValue'); 437 | } 438 | statements.add(sb.toString()); 439 | } 440 | 441 | @override 442 | void set( 443 | String name, 444 | List values, { 445 | bool nullable = false, 446 | String? defaultValue, 447 | String? charset, 448 | String? collate, 449 | unique = false, 450 | }) { 451 | throw UnimplementedError('set not implemented for Postgres'); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /lib/src/database/entity/converter.dart: -------------------------------------------------------------------------------- 1 | part of 'entity.dart'; 2 | 3 | abstract class EntityTypeConverter { 4 | const EntityTypeConverter(); 5 | 6 | Type get _dartType => DartType; 7 | 8 | DBType? toDbType(DartType? value); 9 | 10 | DartType? fromDbType(DBType? value); 11 | } 12 | 13 | Map combineConverters( 14 | List custom, 15 | List driverProvided, 16 | ) { 17 | return { 18 | for (final converter in [...custom, ...driverProvided]) converter._dartType: converter, 19 | }; 20 | } 21 | 22 | Map entityToDbData>(Model entity) { 23 | final typeInfo = Query.getEntity(type: entity.runtimeType); 24 | final typeConverters = combineConverters(typeInfo.converters, entity._driver.typeconverters); 25 | 26 | Object? getValue(DBEntityField field) { 27 | final value = typeInfo.mirror(entity, field.dartName); 28 | final typeConverter = typeConverters[field.type]; 29 | return typeConverter == null ? value : typeConverter.toDbType(value); 30 | } 31 | 32 | return { 33 | for (final entry in typeInfo.editableColumns) entry.columnName: getValue(entry), 34 | }; 35 | } 36 | 37 | @internal 38 | Map entityMapToDbData>( 39 | Map values, 40 | Map typeConverters, { 41 | bool onlyPropertiesPassed = false, 42 | }) { 43 | final entity = Query.getEntity(); 44 | final editableFields = entity.fieldsRequiredForCreate; 45 | 46 | final resultsMap = {}; 47 | 48 | final fieldsToWorkWith = !onlyPropertiesPassed 49 | ? editableFields 50 | : values.keys.map((key) => editableFields.firstWhere((field) => field.dartName == key)); 51 | 52 | for (final field in fieldsToWorkWith) { 53 | var value = values[field.dartName]; 54 | 55 | final typeConverter = typeConverters[field.type]; 56 | value = typeConverter == null ? value : typeConverter.toDbType(value); 57 | if (!field.nullable && value == null) { 58 | throw Exception('Null Value not allowed for Field ${field.dartName} on $T Entity'); 59 | } 60 | 61 | resultsMap[field.columnName] = value; 62 | } 63 | 64 | return resultsMap; 65 | } 66 | 67 | @internal 68 | Model dbDataToEntity>( 69 | Map dataFromDb, 70 | EntityTypeDefinition entity, 71 | Map converters, 72 | ) { 73 | final resultsMap = {}; 74 | for (final entry in entity.columns) { 75 | final value = dataFromDb[entry.columnName]; 76 | final typeConverter = converters[entry.type]; 77 | resultsMap[entry.dartName] = typeConverter == null ? value : typeConverter.fromDbType(value); 78 | } 79 | 80 | return entity.builder(resultsMap); 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/database/entity/entity.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: camel_case_types 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:meta/meta.dart'; 6 | 7 | import 'package:meta/meta_meta.dart'; 8 | 9 | import '../../migration.dart'; 10 | import '../../query/query.dart'; 11 | import '../../reflection.dart'; 12 | import '../../utils.dart'; 13 | import '../database.dart'; 14 | import '../driver/driver.dart'; 15 | 16 | part 'converter.dart'; 17 | part 'relations.dart'; 18 | part 'joins.dart'; 19 | 20 | mixin Entity> { 21 | final Map _relationsPreloaded = {}; 22 | 23 | DriverContract _driver = DB.defaultDriver; 24 | 25 | EntityTypeDefinition get _typeDef => Query.getEntity(); 26 | 27 | String? get connection => null; 28 | 29 | @protected 30 | void initialize() { 31 | if (connection != null) _driver = DB.driver(connection!); 32 | } 33 | 34 | Entity withDriver(DriverContract driver) { 35 | return this.._driver = driver; 36 | } 37 | 38 | @internal 39 | Entity withRelationsData(Map data) { 40 | _relationsPreloaded 41 | ..clear() 42 | ..addAll(data); 43 | return this; 44 | } 45 | 46 | @protected 47 | HasMany hasMany>( 48 | Symbol methodName, { 49 | Symbol? foreignKey, 50 | }) { 51 | final relatedModelTypeData = Query.getEntity(); 52 | foreignKey ??= relatedModelTypeData.bindings.entries.firstWhere((e) => e.value.type == Parent).key; 53 | final referenceField = relatedModelTypeData.columns.firstWhere((e) => e.dartName == foreignKey); 54 | 55 | var relation = _relationsPreloaded[Join._getKey(HasMany, symbolToString(methodName))]; 56 | 57 | if (relation is Map) { 58 | if (relation.isEmpty) { 59 | relation = >[]; 60 | } else { 61 | relation = [relation.cast()]; 62 | } 63 | } 64 | 65 | return HasMany._( 66 | referenceField.columnName, 67 | this as Parent, 68 | relation, 69 | ); 70 | } 71 | 72 | @protected 73 | HasOne hasOne>( 74 | Symbol methodName, { 75 | Symbol? foreignKey, 76 | }) { 77 | final relatedPrimaryKey = Query.getEntity().primaryKey.columnName; 78 | final typeData = Query.getEntity(); 79 | 80 | foreignKey ??= typeData.bindings.entries.firstWhere((e) => e.value.type == RelatedModel).key; 81 | final referenceFieldValue = typeData.mirror(this as Parent, foreignKey); 82 | 83 | return HasOne._( 84 | relatedPrimaryKey, 85 | referenceFieldValue, 86 | this as Parent, 87 | _relationsPreloaded[Join._getKey(HasOne, symbolToString(methodName))], 88 | ); 89 | } 90 | 91 | @protected 92 | BelongsTo belongsTo>( 93 | Symbol methodName, { 94 | Symbol? foreignKey, 95 | }) { 96 | final parentFieldName = Query.getEntity().primaryKey.columnName; 97 | foreignKey ??= _typeDef.bindings.entries.firstWhere((e) => e.value.type == RelatedModel).key; 98 | final referenceFieldValue = _typeDef.mirror(this as Parent, foreignKey); 99 | 100 | return BelongsTo._( 101 | parentFieldName, 102 | referenceFieldValue, 103 | this as Parent, 104 | _relationsPreloaded[Join._getKey(BelongsTo, symbolToString(methodName))], 105 | ); 106 | } 107 | } 108 | 109 | @Target({TargetKind.classType}) 110 | class Table { 111 | final String? name; 112 | final List converters; 113 | 114 | const Table({ 115 | this.name, 116 | this.converters = const [], 117 | }); 118 | } 119 | 120 | @Target({TargetKind.field, TargetKind.parameter}) 121 | class TableColumn { 122 | final String? name; 123 | final bool nullable; 124 | final bool unique; 125 | 126 | const TableColumn({this.name, this.nullable = false, this.unique = false}); 127 | } 128 | 129 | @Target({TargetKind.field, TargetKind.parameter}) 130 | class PrimaryKey extends TableColumn { 131 | final bool autoIncrement; 132 | const PrimaryKey({this.autoIncrement = false, super.name}); 133 | } 134 | 135 | @Target({TargetKind.field, TargetKind.parameter}) 136 | class CreatedAtColumn extends TableColumn { 137 | const CreatedAtColumn() : super(name: 'createdAt', nullable: false); 138 | } 139 | 140 | @Target({TargetKind.field, TargetKind.parameter}) 141 | class UpdatedAtColumn extends TableColumn { 142 | const UpdatedAtColumn() : super(name: 'updatedAt', nullable: false); 143 | } 144 | 145 | /// Use this to reference other entities 146 | @Target({TargetKind.field, TargetKind.parameter}) 147 | class bindTo { 148 | final Type type; 149 | final Symbol? on; 150 | 151 | final ForeignKeyAction? onUpdate, onDelete; 152 | 153 | const bindTo(this.type, {this.on, this.onUpdate, this.onDelete}); 154 | } 155 | 156 | /// A wrapper around arbitrary data [T] to indicate presence or absence 157 | /// explicitly. 158 | /// 159 | /// [Value]s are commonly used in companions to distringuish between `null` and 160 | /// absent values. 161 | /// For instance, consider a table with a nullable column with a non-nullable 162 | /// default value: 163 | /// 164 | /// ```sql 165 | /// CREATE TABLE orders ( 166 | /// priority INT DEFAULT 1 -- may be null if there's no assigned priority 167 | /// ); 168 | /// 169 | /// For inserts in Dart, there are three different scenarios for the `priority` 170 | /// column: 171 | /// 172 | /// - It may be set to `null`, overriding the default value 173 | /// - It may be absent, meaning that the default value should be used 174 | /// - It may be set to an `int` to override the default value 175 | /// ``` 176 | /// 177 | /// As you can see, a simple `int?` does not provide enough information to 178 | /// distinguish between the three cases. A `null` value could mean that the 179 | /// column is absent, or that it should explicitly be set to `null`. 180 | /// For this reason, drift introduces the [Value] wrapper to make the 181 | /// distinction explicit. 182 | class Value { 183 | /// Whether this [Value] wrapper contains a present [value] that should be 184 | /// inserted or updated. 185 | final bool present; 186 | 187 | final T? _value; 188 | 189 | /// If this value is [present], contains the value to update or insert. 190 | T get value => _value as T; 191 | 192 | /// Create a (present) value by wrapping the [value] provided. 193 | const Value(T value) 194 | : _value = value, 195 | present = true; 196 | 197 | /// Create an absent value that will not be written into the database, the 198 | /// default value or null will be used instead. 199 | const Value.absent() 200 | : _value = null, 201 | present = false; 202 | 203 | /// Create a value that is absent if [value] is `null` and [present] if it's 204 | /// not. 205 | /// 206 | /// The functionality is equiavalent to the following: 207 | /// `x != null ? Value(x) : Value.absent()`. 208 | const Value.absentIfNull(T? value) 209 | : _value = value, 210 | present = value != null; 211 | 212 | @override 213 | String toString() => present ? 'Value($value)' : 'Value.absent()'; 214 | 215 | @override 216 | bool operator ==(Object other) => 217 | identical(this, other) || other is Value && present == other.present && _value == other._value; 218 | 219 | @override 220 | int get hashCode => present.hashCode ^ _value.hashCode; 221 | } 222 | -------------------------------------------------------------------------------- /lib/src/database/entity/joins.dart: -------------------------------------------------------------------------------- 1 | part of 'entity.dart'; 2 | 3 | abstract class JoinBuilder> {} 4 | 5 | final class Join, Reference extends Entity> { 6 | final Entry origin; 7 | final Entry on; 8 | 9 | final String resultKey; 10 | 11 | /// This is the key that will be used to store the result of this 12 | /// of this relation in [Entity] relations cache. 13 | final String key; 14 | 15 | Iterable get aliasedForeignSelections => Query.getEntity() 16 | .columns 17 | .map((e) => '"${on.table}"."${e.columnName}" as "$resultKey.${e.columnName}"'); 18 | 19 | const Join( 20 | this.resultKey, { 21 | required this.origin, 22 | required this.on, 23 | required Type key, 24 | }) : key = '$key#$resultKey'; 25 | 26 | static String _getKey(Type type, String methodName) { 27 | return '$type#$methodName'; 28 | } 29 | } 30 | 31 | typedef Entry> = ({String table, String column}); 32 | -------------------------------------------------------------------------------- /lib/src/database/entity/misc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'entity.dart'; 4 | 5 | const dateTimeConverter = DateTimeConverter(); 6 | const booleanConverter = BooleanConverter(); 7 | 8 | const intListConverter = _ListOfObject(); 9 | const stringListConverter = _ListOfObject(); 10 | const numListConverter = _ListOfObject(); 11 | const doubleListConverter = _ListOfObject(); 12 | 13 | const defaultListConverters = [ 14 | intListConverter, 15 | numListConverter, 16 | doubleListConverter, 17 | stringListConverter, 18 | ]; 19 | 20 | class DateTimeConverter extends EntityTypeConverter { 21 | const DateTimeConverter(); 22 | 23 | String padValue(v) => v.toString().padLeft(2, '0'); 24 | 25 | @override 26 | DateTime? fromDbType(String? value) => value == null ? null : DateTime.parse(value); 27 | 28 | @override 29 | String? toDbType(DateTime? value) { 30 | if (value == null) return null; 31 | return '${value.year}-${padValue(value.month)}-${padValue(value.day)} ${padValue(value.hour)}:${padValue(value.minute)}:${padValue(value.second)}'; 32 | } 33 | } 34 | 35 | class BooleanConverter extends EntityTypeConverter { 36 | const BooleanConverter(); 37 | 38 | @override 39 | bool? fromDbType(int? value) => value == null ? null : value != 0; 40 | 41 | @override 42 | int? toDbType(bool? value) => (value == null || value == false) ? 0 : 1; 43 | } 44 | 45 | class _ListOfObject extends EntityTypeConverter, String> { 46 | const _ListOfObject(); 47 | @override 48 | List fromDbType(covariant String value) => List.from(jsonDecode(value)); 49 | 50 | @override 51 | String toDbType(covariant List value) => jsonEncode(value); 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/database/entity/relations.dart: -------------------------------------------------------------------------------- 1 | part of 'entity.dart'; 2 | 3 | abstract class EntityRelation, RelatedModel extends Entity> { 4 | final Parent parent; 5 | 6 | late final Query _query; 7 | 8 | EntityRelation(this.parent) : _query = Query.table().driver(parent._driver); 9 | 10 | Object get parentId { 11 | final typeInfo = parent._typeDef; 12 | return typeInfo.mirror.call(parent, typeInfo.primaryKey.dartName)!; 13 | } 14 | 15 | DriverContract get _driver => parent._driver; 16 | 17 | /// This holds preloaded values from `withRelation` 18 | dynamic get value => 19 | throw StateError('No preloaded data for this relation. Did you forget to call `withRelations` ?'); 20 | 21 | get({bool refresh = false}); 22 | 23 | bool get loaded; 24 | 25 | delete(); 26 | } 27 | 28 | final class HasOne, RelatedModel extends Entity> 29 | extends EntityRelation { 30 | final String _relatedModelPrimaryKey; 31 | final dynamic _relatedModelValue; 32 | final Map? _cache; 33 | 34 | HasOne._( 35 | this._relatedModelPrimaryKey, 36 | this._relatedModelValue, 37 | super._owner, 38 | this._cache, 39 | ); 40 | 41 | @override 42 | bool get loaded => _cache != null; 43 | 44 | @override 45 | RelatedModel? get value { 46 | if (_cache == null) { 47 | return super.value; 48 | } else if (_cache.isEmpty) { 49 | return null; 50 | } 51 | final typeData = Query.getEntity(); 52 | return dbDataToEntity( 53 | _cache, 54 | typeData, 55 | combineConverters(typeData.converters, _driver.typeconverters), 56 | ); 57 | } 58 | 59 | @internal 60 | ReadQuery get $readQuery => _query.where((q) => q.$equal(_relatedModelPrimaryKey, _relatedModelValue)); 61 | 62 | @override 63 | FutureOr get({bool refresh = false}) async { 64 | if (_cache != null && !refresh) return value; 65 | return $readQuery.findOne(); 66 | } 67 | 68 | @override 69 | Future delete() => $readQuery.delete(); 70 | 71 | Future exists() => $readQuery.exists(); 72 | } 73 | 74 | final class HasMany, RelatedModel extends Entity> 75 | extends EntityRelation { 76 | final List>? _cache; 77 | final String foreignKey; 78 | 79 | HasMany._(this.foreignKey, super.parent, this._cache); 80 | 81 | ReadQuery get $readQuery => _query.where((q) => q.$equal(foreignKey, parentId)); 82 | 83 | @override 84 | bool get loaded => _cache != null; 85 | 86 | @override 87 | List get value { 88 | if (_cache == null) { 89 | return super.value; 90 | } else if (_cache.isEmpty) { 91 | return []; 92 | } 93 | 94 | final typeData = Query.getEntity(); 95 | return _cache 96 | .map((data) => dbDataToEntity( 97 | data, 98 | typeData, 99 | combineConverters(typeData.converters, _driver.typeconverters), 100 | )) 101 | .toList(); 102 | } 103 | 104 | @override 105 | FutureOr> get({ 106 | int? limit, 107 | int? offset, 108 | List>? orderBy, 109 | bool refresh = false, 110 | }) async { 111 | if (_cache != null && !refresh) { 112 | return value; 113 | } 114 | 115 | return $readQuery.findMany( 116 | limit: limit, 117 | offset: offset, 118 | orderBy: orderBy, 119 | ); 120 | } 121 | 122 | FutureOr get first => $readQuery.findOne(); 123 | 124 | @override 125 | Future delete() => $readQuery.delete(); 126 | 127 | Future insert(CreateRelatedEntity related) async { 128 | final data = _CreateEntity({...related.toMap, related.field: parentId}); 129 | return $readQuery.$query.insert(data); 130 | } 131 | 132 | Future insertMany(List> related) async { 133 | final data = related.map((e) => _CreateEntity({...e.toMap, e.field: parentId})).toList(); 134 | return $readQuery.$query.insertMany(data); 135 | } 136 | } 137 | 138 | class _CreateEntity> extends CreateEntity { 139 | final Map _data; 140 | const _CreateEntity(this._data); 141 | @override 142 | Map get toMap => _data; 143 | } 144 | 145 | final class BelongsTo, RelatedModel extends Entity> 146 | extends EntityRelation { 147 | final Map? _cache; 148 | final String foreignKey; 149 | final dynamic foreignKeyValue; 150 | 151 | @override 152 | bool get loaded => _cache != null; 153 | 154 | BelongsTo._( 155 | this.foreignKey, 156 | this.foreignKeyValue, 157 | super.parent, 158 | this._cache, 159 | ); 160 | 161 | ReadQuery get _readQuery { 162 | return Query.table().driver(_driver).where((q) => q.$equal( 163 | foreignKey, 164 | foreignKeyValue, 165 | )); 166 | } 167 | 168 | @override 169 | RelatedModel? get value { 170 | if (_cache == null) { 171 | return super.value; 172 | } else if (_cache.isEmpty) { 173 | return null; 174 | } 175 | final typeData = Query.getEntity(); 176 | return dbDataToEntity( 177 | _cache, 178 | typeData, 179 | combineConverters(typeData.converters, _driver.typeconverters), 180 | ); 181 | } 182 | 183 | @override 184 | FutureOr get({bool refresh = false}) async { 185 | if (_cache != null && !refresh) { 186 | return value; 187 | } 188 | return _readQuery.findOne(); 189 | } 190 | 191 | @override 192 | Future delete() => _readQuery.delete(); 193 | } 194 | -------------------------------------------------------------------------------- /lib/src/migration.dart: -------------------------------------------------------------------------------- 1 | library migration; 2 | 3 | import 'package:meta/meta.dart'; 4 | import 'package:recase/recase.dart'; 5 | 6 | import 'database/entity/entity.dart'; 7 | import 'query/query.dart'; 8 | import 'reflection.dart'; 9 | 10 | abstract class TableBlueprint { 11 | void id({String name = 'id', String? type, bool autoIncrement = true}); 12 | 13 | void foreign(ForeignKey key); 14 | 15 | void string(String name, {bool nullable = false, String? defaultValue, bool unique = false}); 16 | 17 | void boolean(String name, {bool nullable = false, bool? defaultValue}); 18 | 19 | void timestamp(String name, {bool nullable = false, DateTime? defaultValue, bool unique = false}); 20 | 21 | void datetime(String name, {bool nullable = false, DateTime? defaultValue}); 22 | 23 | void date(String name, {bool nullable = false, DateTime? defaultValue}); 24 | 25 | void time(String name, {bool nullable = false, DateTime? defaultValue}); 26 | 27 | void blob(String name, {bool nullable = false, String? defaultValue}); 28 | 29 | void timestamps({ 30 | String createdAt = 'createdAt', 31 | String updatedAt = 'updatedAt', 32 | }); 33 | 34 | /// NUMBER TYPES 35 | /// ---------------------------------------------------------------- 36 | 37 | void integer(String name, {bool nullable = false, num? defaultValue, bool unique = false}); 38 | 39 | void double(String name, {bool nullable = false, num? defaultValue, int? precision, int? scale, bool unique = false}); 40 | 41 | void float(String name, {bool nullable = false, num? defaultValue, int? precision, int? scale, bool unique = false}); 42 | 43 | void tinyInt(String name, {bool nullable = false, num? defaultValue, bool unique = false}); 44 | 45 | void smallInteger(String name, {bool nullable = false, num? defaultValue, bool unique = false}); 46 | 47 | void mediumInteger(String name, {bool nullable = false, num? defaultValue, bool unique = false}); 48 | 49 | void bigInteger(String name, {bool nullable = false, num? defaultValue, bool unique = false}); 50 | 51 | void decimal( 52 | String name, { 53 | bool nullable = false, 54 | num? defaultValue, 55 | int? precision, 56 | int? scale, 57 | bool unique = false, 58 | }); 59 | 60 | void numeric( 61 | String name, { 62 | bool nullable = false, 63 | num? defaultValue, 64 | int? precision, 65 | int? scale, 66 | bool unique = false, 67 | }); 68 | 69 | void bit(String name, {bool nullable = false, int? defaultValue, bool unique = false}); 70 | 71 | /// STRING TYPES 72 | /// ---------------------------------------------------------------- 73 | 74 | void text( 75 | String name, { 76 | int length = 1, 77 | bool nullable = false, 78 | String? defaultValue, 79 | String? charset, 80 | String? collate, 81 | bool unique = false, 82 | }); 83 | 84 | void char( 85 | String name, { 86 | bool nullable = false, 87 | int length = 1, 88 | String? defaultValue, 89 | String? charset, 90 | String? collate, 91 | bool unique = false, 92 | }); 93 | 94 | void varchar( 95 | String name, { 96 | bool nullable = false, 97 | String? defaultValue, 98 | int length = 255, 99 | String? charset, 100 | String? collate, 101 | bool unique = false, 102 | }); 103 | 104 | void tinyText( 105 | String name, { 106 | bool nullable = false, 107 | String? defaultValue, 108 | String? charset, 109 | String? collate, 110 | bool unique = false, 111 | }); 112 | 113 | void mediumText( 114 | String name, { 115 | bool nullable = false, 116 | String? defaultValue, 117 | String? charset, 118 | String? collate, 119 | bool unique = false, 120 | }); 121 | 122 | void longText( 123 | String name, { 124 | bool nullable = false, 125 | String? defaultValue, 126 | String? charset, 127 | String? collate, 128 | bool unique = false, 129 | }); 130 | 131 | void binary( 132 | String name, { 133 | bool nullable = false, 134 | int size = 1, 135 | String? defaultValue, 136 | String? charset, 137 | String? collate, 138 | }); 139 | 140 | void varbinary( 141 | String name, { 142 | bool nullable = false, 143 | int size = 1, 144 | String? defaultValue, 145 | String? charset, 146 | String? collate, 147 | }); 148 | 149 | void enums( 150 | String name, 151 | List values, { 152 | bool nullable = false, 153 | String? defaultValue, 154 | String? charset, 155 | String? collate, 156 | bool unique = false, 157 | }); 158 | 159 | void set( 160 | String name, 161 | List values, { 162 | bool nullable = false, 163 | String? defaultValue, 164 | String? charset, 165 | String? collate, 166 | bool unique = false, 167 | }); 168 | 169 | @protected 170 | String createScript(String tableName); 171 | 172 | @protected 173 | String dropScript(String tableName); 174 | 175 | @protected 176 | String renameScript(String fromName, String toName); 177 | 178 | void ensurePresenceOf(String column); 179 | } 180 | 181 | typedef TableBluePrintFunc = TableBlueprint Function(TableBlueprint table); 182 | 183 | abstract class Schema { 184 | final String tableName; 185 | final TableBluePrintFunc? _bluePrintFunc; 186 | 187 | Schema._(this.tableName, this._bluePrintFunc); 188 | 189 | String toScript(TableBlueprint table); 190 | 191 | static CreateSchema fromEntity>() { 192 | final entity = Query.getEntity(); 193 | 194 | TableBlueprint make(TableBlueprint table, DBEntityField field) => switch (field.type) { 195 | const (int) => table..integer(field.columnName, nullable: field.nullable, unique: field.unique), 196 | const (double) || const (num) => table 197 | ..double(field.columnName, nullable: field.nullable, unique: field.unique), 198 | const (DateTime) => table..datetime(field.columnName, nullable: field.nullable), 199 | _ => table..string(field.columnName, nullable: field.nullable, unique: field.unique), 200 | }; 201 | 202 | return CreateSchema._( 203 | entity.tableName, 204 | (table) { 205 | final primaryKey = entity.primaryKey; 206 | table.id( 207 | name: entity.primaryKey.columnName, 208 | autoIncrement: entity.primaryKey.autoIncrement, 209 | 210 | /// TODO(codekeyz): is this the right way to do this? 211 | type: primaryKey.type == String ? 'VARCHAR(255)' : null, 212 | ); 213 | 214 | for (final prop in entity.columns.where((e) => !e.isPrimaryKey)) { 215 | table = make(table, prop); 216 | } 217 | 218 | for (final key in entity.bindings.keys) { 219 | final binding = entity.bindings[key]!; 220 | final prop = entity.columns.firstWhere((e) => e.dartName == key); 221 | final referenceTypeData = binding.referenceTypeDef; 222 | final referenceColumn = binding.reference; 223 | 224 | final foreignKey = ForeignKey( 225 | entity.tableName, 226 | prop.columnName, 227 | foreignTable: referenceTypeData.tableName, 228 | foreignTableColumn: referenceColumn.columnName, 229 | onUpdate: binding.onUpdate, 230 | onDelete: binding.onDelete, 231 | ); 232 | 233 | table.foreign(foreignKey); 234 | } 235 | 236 | return table; 237 | }, 238 | ); 239 | } 240 | 241 | static Schema dropIfExists(CreateSchema value) => _DropSchema(value.tableName); 242 | 243 | static Schema rename(String from, String to) => _RenameSchema(from, to); 244 | } 245 | 246 | abstract class Migration { 247 | final String? connection; 248 | 249 | const Migration({this.connection}); 250 | 251 | String get name => runtimeType.toString().snakeCase; 252 | 253 | void up(List schemas); 254 | 255 | void down(List schemas); 256 | } 257 | 258 | final class CreateSchema> extends Schema { 259 | CreateSchema._(super.name, super.func) : super._(); 260 | 261 | @override 262 | String toScript(TableBlueprint table) { 263 | table = _bluePrintFunc!.call(table); 264 | return table.createScript(tableName); 265 | } 266 | } 267 | 268 | class _DropSchema extends Schema { 269 | _DropSchema(String name) : super._(name, null); 270 | 271 | @override 272 | String toScript(TableBlueprint table) => table.dropScript(tableName); 273 | } 274 | 275 | class _RenameSchema extends Schema { 276 | final String newName; 277 | 278 | _RenameSchema(String from, this.newName) : super._(from, null); 279 | 280 | @override 281 | String toScript(TableBlueprint table) => table.renameScript(tableName, newName); 282 | } 283 | 284 | enum ForeignKeyAction { cascade, restrict, setNull, setDefault, noAction } 285 | 286 | class ForeignKey { 287 | final String table; 288 | final String column; 289 | 290 | final String foreignTable; 291 | final String foreignTableColumn; 292 | 293 | final bool nullable; 294 | 295 | final ForeignKeyAction? onUpdate; 296 | final ForeignKeyAction? onDelete; 297 | 298 | final String? constraint; 299 | 300 | const ForeignKey( 301 | this.table, 302 | this.column, { 303 | required this.foreignTable, 304 | required this.foreignTableColumn, 305 | this.nullable = false, 306 | this.onUpdate, 307 | this.onDelete, 308 | this.constraint, 309 | }); 310 | 311 | ForeignKey actions({ 312 | ForeignKeyAction? onUpdate, 313 | ForeignKeyAction? onDelete, 314 | }) => 315 | ForeignKey( 316 | table, 317 | column, 318 | foreignTable: foreignTable, 319 | foreignTableColumn: foreignTableColumn, 320 | nullable: nullable, 321 | constraint: constraint, 322 | onUpdate: onUpdate ?? this.onUpdate, 323 | onDelete: onDelete ?? this.onDelete, 324 | ); 325 | 326 | ForeignKey constrained({String? name}) => ForeignKey( 327 | table, 328 | column, 329 | foreignTable: foreignTable, 330 | foreignTableColumn: foreignTableColumn, 331 | nullable: nullable, 332 | onUpdate: onUpdate, 333 | onDelete: onDelete, 334 | constraint: name ?? 'fk_${table}_${column}_to_${foreignTable}_$foreignTableColumn', 335 | ); 336 | } 337 | -------------------------------------------------------------------------------- /lib/src/primitives/serializer.dart: -------------------------------------------------------------------------------- 1 | import '../migration.dart'; 2 | import '../query/aggregates.dart'; 3 | 4 | import '../query/query.dart'; 5 | 6 | abstract class PrimitiveSerializer { 7 | const PrimitiveSerializer(); 8 | 9 | String acceptAggregate(AggregateFunction aggregate); 10 | 11 | String acceptReadQuery(ReadQuery query); 12 | 13 | String acceptUpdateQuery(UpdateQuery query); 14 | 15 | String acceptDeleteQuery(DeleteQuery query); 16 | 17 | String acceptInsertQuery(InsertQuery query); 18 | 19 | String acceptInsertManyQuery(InsertManyQuery query); 20 | 21 | String acceptWhereClauseValue(WhereClauseValue clauseValue); 22 | 23 | String acceptSelect(String tableName, List fields); 24 | 25 | String acceptOrderBy(String tableName, List orderBys); 26 | 27 | String acceptLimit(int limit); 28 | 29 | String acceptOffset(int offset); 30 | 31 | dynamic acceptPrimitiveValue(dynamic value); 32 | 33 | String acceptForeignKey(TableBlueprint blueprint, ForeignKey key); 34 | 35 | String escapeStr(String column); 36 | 37 | String get terminator; 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/primitives/where.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | 3 | part of '../query/query.dart'; 4 | 5 | typedef WhereBuilder> = WhereClause Function(WhereClauseBuilder builder); 6 | 7 | $AndGroup and(List values) => $AndGroup._(values); 8 | 9 | $OrGroup or(List values) => $OrGroup._(values); 10 | 11 | mixin WhereOperation { 12 | WhereClauseValue $equal(String field, T value); 13 | 14 | WhereClauseValue $notEqual(String field, T value); 15 | 16 | WhereClauseValue $isNull(String field); 17 | 18 | WhereClauseValue $isNotNull(String field); 19 | 20 | WhereClauseValue> $isIn(String field, List values); 21 | 22 | WhereClauseValue> $isNotIn(String field, List values); 23 | 24 | WhereClauseValue $isLike(String field, String pattern); 25 | 26 | WhereClauseValue $isNotLike(String field, String pattern); 27 | 28 | WhereClauseValue> $isBetween(String field, List values); 29 | 30 | WhereClauseValue> $isNotBetween(String field, List values); 31 | } 32 | 33 | enum LogicalOperator { AND, OR } 34 | 35 | typedef CombineClause = (LogicalOperator operator, T clause); 36 | 37 | enum Operator { 38 | IN, 39 | NOT_IN, 40 | NULL, 41 | LIKE, 42 | NOT_LIKE, 43 | BETWEEN, 44 | NOT_BETWEEN, 45 | EQUAL, 46 | NOT_EQUAL, 47 | NOT_NULL, 48 | LESS_THAN, 49 | GREAT_THAN, 50 | GREATER_THAN_OR_EQUAL_TO, 51 | LESS_THEN_OR_EQUAL_TO, 52 | } 53 | 54 | typedef CompareWithValue = ({Operator operator, Value? value}); 55 | 56 | abstract interface class WhereClause { 57 | final List values; 58 | const WhereClause(this.values, {String? table}); 59 | 60 | @internal 61 | void validate(List joins); 62 | 63 | void _withConverters(Map converters); 64 | } 65 | 66 | class $AndGroup extends WhereClause { 67 | const $AndGroup._(super.values); 68 | 69 | @override 70 | void validate(List joins) { 71 | final clauseValues = values.whereType(); 72 | for (final val in clauseValues) { 73 | val.validate(joins); 74 | } 75 | } 76 | 77 | @override 78 | void _withConverters(Map converters) { 79 | final clauseValues = values.whereType(); 80 | for (final val in clauseValues) { 81 | val._withConverters(converters); 82 | } 83 | } 84 | } 85 | 86 | class $OrGroup extends WhereClause { 87 | const $OrGroup._(super.values); 88 | 89 | @override 90 | void validate(List joins) { 91 | final clauseValues = values.whereType(); 92 | for (final val in clauseValues) { 93 | val.validate(joins); 94 | } 95 | } 96 | 97 | @override 98 | void _withConverters(Map converters) { 99 | final clauseValues = values.whereType(); 100 | for (final val in clauseValues) { 101 | val._withConverters(converters); 102 | } 103 | } 104 | } 105 | 106 | class WhereClauseValue extends WhereClause { 107 | final String field; 108 | final Operator operator; 109 | final ValueType value; 110 | 111 | final String table; 112 | 113 | final Map _converters = {}; 114 | 115 | WhereClauseValue._( 116 | this.field, 117 | this.operator, 118 | this.value, { 119 | required this.table, 120 | }) : super(const []) { 121 | if ([Operator.BETWEEN, Operator.NOT_BETWEEN].contains(operator)) { 122 | if (value is! Iterable || (value as Iterable).length != 2) { 123 | throw ArgumentError( 124 | '${operator.name} requires a List with length 2 (val1, val2)', 125 | '$field ${operator.name} $value', 126 | ); 127 | } 128 | } 129 | } 130 | 131 | dynamic get dbValue { 132 | final typeConverter = _converters[ValueType]; 133 | if (typeConverter == null) return value; 134 | return typeConverter.toDbType(value); 135 | } 136 | 137 | @override 138 | void validate(List joins) { 139 | if (joins.isNotEmpty) { 140 | final tableJoined = joins.any((e) => [e.on.table, e.origin.table].contains(table)); 141 | if (!tableJoined) { 142 | throw ArgumentError( 143 | 'No Joins found to enable `$table.$field ${operator.name} $value` Did you forget to call `.withRelations` ?', 144 | ); 145 | } 146 | } 147 | } 148 | 149 | WhereClause operator &(WhereClauseValue other) => and([this, other]); 150 | 151 | WhereClause operator |(WhereClauseValue other) => or([this, other]); 152 | 153 | @override 154 | void _withConverters(Map converters) { 155 | _converters 156 | ..clear() 157 | ..addAll(converters); 158 | } 159 | } 160 | 161 | class WhereClauseBuilder> with WhereOperation { 162 | final String table; 163 | 164 | WhereClauseBuilder() : table = Query.getEntity().tableName; 165 | 166 | @override 167 | WhereClauseValue $equal(String field, V value) { 168 | _ensureHasField(field); 169 | return WhereClauseValue._(field, Operator.EQUAL, value, table: table); 170 | } 171 | 172 | @override 173 | WhereClauseValue $notEqual(String field, V value) { 174 | _ensureHasField(field); 175 | return WhereClauseValue._(field, Operator.NOT_EQUAL, value, table: table); 176 | } 177 | 178 | @override 179 | WhereClauseValue> $isIn(String field, List values) { 180 | _ensureHasField(field); 181 | return WhereClauseValue._(field, Operator.IN, values, table: table); 182 | } 183 | 184 | @override 185 | WhereClauseValue> $isNotIn(String field, List values) { 186 | _ensureHasField(field); 187 | return WhereClauseValue._(field, Operator.NOT_IN, values, table: table); 188 | } 189 | 190 | @override 191 | WhereClauseValue $isLike(String field, String pattern) { 192 | _ensureHasField(field); 193 | return WhereClauseValue._(field, Operator.LIKE, pattern, table: table); 194 | } 195 | 196 | @override 197 | WhereClauseValue $isNotLike(String field, String pattern) { 198 | _ensureHasField(field); 199 | return WhereClauseValue._(field, Operator.NOT_LIKE, pattern, table: table); 200 | } 201 | 202 | @override 203 | WhereClauseValue $isNull(String field) { 204 | _ensureHasField(field); 205 | return WhereClauseValue._(field, Operator.NULL, null, table: table); 206 | } 207 | 208 | @override 209 | WhereClauseValue $isNotNull(String field) { 210 | _ensureHasField(field); 211 | return WhereClauseValue._(field, Operator.NOT_NULL, null, table: table); 212 | } 213 | 214 | @override 215 | WhereClauseValue> $isBetween(String field, List values) { 216 | _ensureHasField(field); 217 | return WhereClauseValue._(field, Operator.BETWEEN, values, table: table); 218 | } 219 | 220 | @override 221 | WhereClauseValue> $isNotBetween(String field, List values) { 222 | _ensureHasField(field); 223 | return WhereClauseValue._(field, Operator.NOT_BETWEEN, values, table: table); 224 | } 225 | 226 | void _ensureHasField(String field) { 227 | final typeData = Query.getEntity(); 228 | final hasField = typeData.columns.any((e) => e.columnName == field); 229 | if (!hasField) { 230 | throw ArgumentError( 231 | 'Field `${typeData.tableName}.$field` not found on $T Entity. Did you mis-spell it ?', 232 | ); 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/src/query/aggregates.dart: -------------------------------------------------------------------------------- 1 | import '../database/driver/driver.dart'; 2 | import 'query.dart'; 3 | 4 | sealed class AggregateFunction { 5 | final List selections; 6 | final WhereClause? whereClause; 7 | final String tableName; 8 | final DriverContract driver; 9 | 10 | List get arguments { 11 | if (selections.isEmpty) return ['*']; 12 | return selections; 13 | } 14 | 15 | String get name; 16 | 17 | const AggregateFunction( 18 | this.driver, 19 | this.tableName, { 20 | this.selections = const [], 21 | this.whereClause, 22 | }); 23 | 24 | AggregateFunction._init(ReadQuery query, String? field) 25 | : driver = query.runner, 26 | tableName = query.tableName, 27 | whereClause = query.whereClause, 28 | selections = field == null ? const [] : [field], 29 | assert( 30 | query.fieldSelections.isEmpty, 31 | 'You can not use selections with aggregate functisons', 32 | ); 33 | 34 | Future get() async { 35 | final statement = driver.serializer.acceptAggregate(this); 36 | final result = await driver.rawQuery(statement); 37 | final value = result[0].values.first; 38 | if (value is T) return value; 39 | return switch (T) { 40 | const (int) || const (double) || const (num) => value == null ? 0 : num.parse(value.toString()), 41 | const (String) => value.toString(), 42 | _ => throw Exception('Null value returned for aggregate: $statement'), 43 | } as T; 44 | } 45 | 46 | String get statement => driver.serializer.acceptAggregate(this); 47 | } 48 | 49 | class CountAggregate extends AggregateFunction { 50 | final bool distinct; 51 | 52 | CountAggregate(super.query, super.field, this.distinct) : super._init(); 53 | 54 | @override 55 | String get name => 'COUNT'; 56 | } 57 | 58 | class SumAggregate extends AggregateFunction { 59 | SumAggregate(super.query, String super.field) : super._init(); 60 | 61 | @override 62 | String get name => 'SUM'; 63 | } 64 | 65 | class AverageAggregate extends AggregateFunction { 66 | AverageAggregate(super.query, String super.field) : super._init(); 67 | 68 | @override 69 | String get name => 'AVG'; 70 | } 71 | 72 | class MaxAggregate extends AggregateFunction { 73 | MaxAggregate(super.query, String super.field) : super._init(); 74 | 75 | @override 76 | String get name => 'MAX'; 77 | } 78 | 79 | class MinAggregate extends AggregateFunction { 80 | MinAggregate(super.query, String super.field) : super._init(); 81 | 82 | @override 83 | String get name => 'MIN'; 84 | } 85 | 86 | class GroupConcatAggregate extends AggregateFunction { 87 | final String separator; 88 | 89 | GroupConcatAggregate(super.query, String super.field, this.separator) : super._init(); 90 | 91 | @override 92 | List get arguments { 93 | final separatorStr = "'$separator'"; 94 | 95 | if (driver.type == DatabaseDriverType.pgsql) { 96 | return ['ARRAY_AGG(${selections.first})', separatorStr]; 97 | } 98 | 99 | return [...super.arguments, separatorStr]; 100 | } 101 | 102 | @override 103 | String get name { 104 | if (driver.type == DatabaseDriverType.pgsql) return 'ARRAY_TO_STRING'; 105 | return 'GROUP_CONCAT'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/query/query.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../database/driver/driver.dart'; 4 | import '../database/entity/entity.dart'; 5 | import '../reflection.dart'; 6 | import 'aggregates.dart'; 7 | 8 | part '../primitives/where.dart'; 9 | 10 | enum OrderDirection { asc, desc } 11 | 12 | abstract class CreateEntity> { 13 | const CreateEntity(); 14 | @internal 15 | Map get toMap; 16 | } 17 | 18 | abstract class UpdateEntity> { 19 | const UpdateEntity(); 20 | @internal 21 | Map get toMap; 22 | } 23 | 24 | abstract class CreateRelatedEntity, RelatedModel extends Entity> { 25 | const CreateRelatedEntity(); 26 | 27 | Symbol get field; 28 | 29 | @internal 30 | Map get toMap; 31 | } 32 | 33 | mixin ReadOperation> { 34 | Future findOne({ 35 | WhereBuilder? where, 36 | }); 37 | 38 | Future> findMany({ 39 | WhereBuilder? where, 40 | List> orderBy, 41 | int? limit, 42 | int? offset, 43 | }); 44 | } 45 | 46 | mixin InsertOperation> { 47 | Future insert(CreateEntity data); 48 | Future insertMany(List> datas); 49 | } 50 | 51 | mixin UpdateOperation> { 52 | UpdateQuery update({ 53 | required WhereBuilder where, 54 | required UpdateEntity update, 55 | }); 56 | } 57 | 58 | mixin RelationsOperation> { 59 | withRelations(List> Function(JoinBuilder builder) builder) { 60 | return this; 61 | } 62 | } 63 | 64 | mixin LimitOperation { 65 | Future> take(int limit); 66 | } 67 | 68 | abstract class OrderBy> { 69 | final String field; 70 | final OrderDirection direction; 71 | 72 | const OrderBy(this.field, this.direction); 73 | } 74 | 75 | sealed class QueryBase { 76 | final String tableName; 77 | final String? database; 78 | 79 | final Query $query; 80 | 81 | DriverContract get runner => $query.runner; 82 | 83 | Future execute(); 84 | 85 | QueryBase(this.$query) 86 | : tableName = $query.tableName, 87 | database = $query.database; 88 | 89 | String get statement; 90 | } 91 | 92 | final class Query> 93 | with ReadOperation, InsertOperation, UpdateOperation, AggregateOperation, RelationsOperation { 94 | final EntityTypeDefinition entity; 95 | final String? database; 96 | final List _joins; 97 | 98 | late final String tableName; 99 | 100 | DriverContract? _queryDriver; 101 | 102 | Map get converters => combineConverters(entity.converters, runner.typeconverters); 103 | 104 | static final Map _typedatas = {}; 105 | 106 | Query._({String? tableName, this.database}) 107 | : entity = Query.getEntity(), 108 | _joins = [] { 109 | this.tableName = tableName ?? entity.tableName; 110 | } 111 | 112 | DriverContract get runner { 113 | if (_queryDriver == null) { 114 | throw StateError('Driver not set for query. Make sure you supply a driver using .driver()'); 115 | } 116 | return _queryDriver!; 117 | } 118 | 119 | Query driver(DriverContract driver) { 120 | _queryDriver = driver; 121 | return this; 122 | } 123 | 124 | static Query table>([String? tableName, String? database]) { 125 | if (Model == Entity || Model == dynamic) { 126 | throw UnsupportedError('Query cannot receive Entity or dynamic as Type'); 127 | } 128 | return Query._(tableName: tableName, database: database); 129 | } 130 | 131 | static void addTypeDef>(EntityTypeDefinition entity) { 132 | var type = T; 133 | if (type == Entity) type = entity.dartType; 134 | if (type == Entity) throw Exception(); 135 | _typedatas[type] = entity; 136 | } 137 | 138 | @internal 139 | static EntityTypeDefinition getEntity>({Type? type}) { 140 | type ??= T; 141 | if (!_typedatas.containsKey(type)) { 142 | throw Exception('Type Data not found for $type'); 143 | } 144 | return _typedatas[type]! as dynamic; 145 | } 146 | 147 | ReadQuery where(WhereBuilder builder) { 148 | final whereClause = builder.call(WhereClauseBuilder()); 149 | whereClause 150 | ..validate(_joins) 151 | .._withConverters(converters); 152 | 153 | return ReadQuery._(this, whereClause: whereClause); 154 | } 155 | 156 | Map _prepareCreate(CreateEntity data) { 157 | final dataMap = data.toMap; 158 | if (entity.timestampsEnabled) { 159 | final now = DateTime.now(); 160 | final createdAtField = entity.createdAtField; 161 | final updatedAtField = entity.updatedAtField; 162 | 163 | if (createdAtField != null) { 164 | dataMap[createdAtField.dartName] = now; 165 | } 166 | 167 | if (updatedAtField != null) { 168 | dataMap[updatedAtField.dartName] = now; 169 | } 170 | } 171 | return entityMapToDbData(dataMap, converters); 172 | } 173 | 174 | @override 175 | Future insert(CreateEntity data) async { 176 | final dataMap = _prepareCreate(data); 177 | var recordId = await runner.insert( 178 | InsertQuery( 179 | this, 180 | data: dataMap, 181 | primaryKey: entity.primaryKey.columnName, 182 | ), 183 | ); 184 | if (!entity.primaryKey.autoIncrement) { 185 | recordId = data.toMap[entity.primaryKey.dartName].toString(); 186 | } 187 | 188 | return (await findOne(where: (q) => q.$equal(entity.primaryKey.columnName, recordId)))!; 189 | } 190 | 191 | @override 192 | Future insertMany(List> datas) async { 193 | final dataMap = datas.map(_prepareCreate).toList(); 194 | await runner.insertMany( 195 | InsertManyQuery( 196 | this, 197 | values: dataMap, 198 | primaryKey: entity.primaryKey.columnName, 199 | ), 200 | ); 201 | } 202 | 203 | @override 204 | Future> findMany({ 205 | WhereBuilder? where, 206 | List>? orderBy, 207 | int? limit, 208 | int? offset, 209 | }) async { 210 | final whereClause = where?.call(WhereClauseBuilder()); 211 | whereClause?.validate(_joins); 212 | 213 | final readQ = ReadQuery._( 214 | this, 215 | limit: limit, 216 | offset: offset, 217 | whereClause: whereClause, 218 | orderByProps: orderBy?.toSet(), 219 | joins: _joins, 220 | groupBys: [entity.primaryKey.columnName], 221 | ); 222 | 223 | final results = await runner.query(readQ); 224 | if (results.isEmpty) return []; 225 | return results.map(_wrapRawResult).toList(); 226 | } 227 | 228 | @override 229 | Future findOne({WhereBuilder? where}) async { 230 | final whereClause = where?.call(WhereClauseBuilder()); 231 | whereClause?.validate(_joins); 232 | 233 | final readQ = ReadQuery._(this, limit: 1, whereClause: whereClause, joins: _joins); 234 | final results = await runner.query(readQ); 235 | if (results.isEmpty) return null; 236 | return results.map(_wrapRawResult).first; 237 | } 238 | 239 | @override 240 | UpdateQuery update({ 241 | required WhereBuilder where, 242 | required UpdateEntity update, 243 | }) { 244 | final whereClause = where.call(WhereClauseBuilder()); 245 | whereClause.validate(_joins); 246 | 247 | final values = update.toMap; 248 | 249 | if (entity.timestampsEnabled) { 250 | final now = DateTime.now(); 251 | final updatedAtField = entity.updatedAtField; 252 | if (updatedAtField != null) { 253 | values[updatedAtField.dartName] = now; 254 | } 255 | } 256 | 257 | final dataToDbD = entityMapToDbData( 258 | values, 259 | converters, 260 | onlyPropertiesPassed: true, 261 | ); 262 | 263 | return UpdateQuery(this, whereClause: whereClause, data: dataToDbD); 264 | } 265 | 266 | /// [T] is the expected type passed to [Query] via Query 267 | T _wrapRawResult(Map result) { 268 | final Map> joinResults = {}; 269 | for (final join in _joins) { 270 | final entries = result.entries 271 | .where((e) => e.key.startsWith('${join.resultKey}.')) 272 | .map((e) => MapEntry(e.key.replaceFirst('${join.resultKey}.', '').trim(), e.value)); 273 | if (entries.every((e) => e.value == null)) { 274 | joinResults[join.key] = {}; 275 | } else { 276 | joinResults[join.key] = {}..addEntries(entries); 277 | } 278 | } 279 | 280 | return dbDataToEntity( 281 | result, 282 | entity, 283 | converters, 284 | ).withRelationsData(joinResults).withDriver(runner) as T; 285 | } 286 | 287 | ReadQuery get _readQuery => ReadQuery._(this); 288 | 289 | @override 290 | Future average(String field) { 291 | return AverageAggregate(_readQuery, field).get(); 292 | } 293 | 294 | @override 295 | Future count({String? field, bool distinct = false}) { 296 | return CountAggregate(_readQuery, field, distinct).get(); 297 | } 298 | 299 | @override 300 | Future groupConcat(String field, String separator) { 301 | return GroupConcatAggregate(_readQuery, field, separator).get(); 302 | } 303 | 304 | @override 305 | Future max(String field) => MaxAggregate(_readQuery, field).get(); 306 | 307 | @override 308 | Future min(String field) => MinAggregate(_readQuery, field).get(); 309 | 310 | @override 311 | Future sum(String field) => SumAggregate(_readQuery, field).get(); 312 | 313 | @override 314 | Query withRelations(List> Function(JoinBuilder builder) builder) { 315 | _joins 316 | ..clear() 317 | ..addAll(builder.call(_JoinBuilderImpl())); 318 | return this; 319 | } 320 | } 321 | 322 | mixin AggregateOperation { 323 | Future count({String? field, bool distinct = false}); 324 | 325 | Future average(String field); 326 | 327 | Future sum(String field); 328 | 329 | Future max(String field); 330 | 331 | Future min(String field); 332 | 333 | Future groupConcat(String field, String separator); 334 | } 335 | 336 | @protected 337 | final class UpdateQuery extends QueryBase { 338 | final WhereClause whereClause; 339 | final Map data; 340 | 341 | UpdateQuery(super.tableName, {required this.whereClause, required this.data}); 342 | 343 | @override 344 | String get statement => $query.runner.serializer.acceptUpdateQuery(this); 345 | 346 | @override 347 | Future execute() => runner.update(this); 348 | } 349 | 350 | final class ReadQuery> extends QueryBase with AggregateOperation, RelationsOperation { 351 | final Set fieldSelections; 352 | final Set>? orderByProps; 353 | final WhereClause? whereClause; 354 | final List joins; 355 | final int? limit, offset; 356 | final List groupBys; 357 | 358 | ReadQuery._( 359 | Query super.query, { 360 | this.whereClause, 361 | this.orderByProps, 362 | this.fieldSelections = const {}, 363 | this.joins = const [], 364 | this.groupBys = const [], 365 | this.limit, 366 | this.offset, 367 | }); 368 | 369 | @override 370 | Query get $query => (super.$query) as Query; 371 | 372 | @override 373 | String get statement => runner.serializer.acceptReadQuery(this); 374 | 375 | @override 376 | Future>> execute() => runner.query(this); 377 | 378 | @override 379 | Future average(String field) { 380 | return AverageAggregate(this, field).get(); 381 | } 382 | 383 | @override 384 | Future count({String? field, bool distinct = false}) { 385 | return CountAggregate(this, field, distinct).get(); 386 | } 387 | 388 | @override 389 | Future groupConcat(String field, String separator) { 390 | return GroupConcatAggregate(this, field, separator).get(); 391 | } 392 | 393 | @override 394 | Future max(String field) => MaxAggregate(this, field).get(); 395 | 396 | @override 397 | Future min(String field) => MinAggregate(this, field).get(); 398 | 399 | @override 400 | Future sum(String field) { 401 | return SumAggregate(this, field).get(); 402 | } 403 | 404 | Future> findMany({int? limit, int? offset, List>? orderBy}) => $query.findMany( 405 | limit: limit, 406 | offset: offset, 407 | where: (_) => whereClause!, 408 | orderBy: orderBy, 409 | ); 410 | 411 | Future exists() async { 412 | final existsQuery = ReadQuery._( 413 | $query, 414 | fieldSelections: {$query.entity.primaryKey.columnName}, 415 | whereClause: whereClause, 416 | limit: 1, 417 | ); 418 | final result = await $query.runner.query(existsQuery); 419 | return result.isNotEmpty; 420 | } 421 | 422 | Future findOne() => $query.findOne(where: (_) => whereClause!); 423 | 424 | Future delete() => DeleteQuery( 425 | $query, 426 | whereClause: whereClause!, 427 | ).execute(); 428 | 429 | @override 430 | ReadQuery withRelations( 431 | List> Function(JoinBuilder builder) builder, 432 | ) { 433 | joins 434 | ..clear() 435 | ..addAll(builder.call(_JoinBuilderImpl())); 436 | return this; 437 | } 438 | 439 | Future update(UpdateEntity update) => $query 440 | .update( 441 | where: (_) => whereClause!, 442 | update: update, 443 | ) 444 | .execute(); 445 | } 446 | 447 | final class _JoinBuilderImpl> extends JoinBuilder {} 448 | 449 | final class InsertQuery extends QueryBase { 450 | final Map data; 451 | final String primaryKey; 452 | 453 | InsertQuery( 454 | super._query, { 455 | required this.data, 456 | required this.primaryKey, 457 | }); 458 | 459 | @override 460 | Future execute() => runner.insert(this); 461 | 462 | @override 463 | String get statement => runner.serializer.acceptInsertQuery(this); 464 | } 465 | 466 | final class InsertManyQuery extends QueryBase { 467 | final String primaryKey; 468 | final List> values; 469 | 470 | InsertManyQuery( 471 | super.tableName, { 472 | required this.values, 473 | required this.primaryKey, 474 | }); 475 | 476 | @override 477 | String get statement => runner.serializer.acceptInsertManyQuery(this); 478 | 479 | @override 480 | Future execute() => runner.insertMany(this); 481 | } 482 | 483 | @protected 484 | final class DeleteQuery extends QueryBase { 485 | final WhereClause whereClause; 486 | 487 | DeleteQuery(super._query, {required this.whereClause}); 488 | 489 | @override 490 | String get statement => runner.serializer.acceptDeleteQuery(this); 491 | 492 | @override 493 | Future execute() => runner.delete(this); 494 | } 495 | -------------------------------------------------------------------------------- /lib/src/reflection.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart'; 2 | 3 | import 'database/entity/entity.dart'; 4 | import 'query/query.dart'; 5 | 6 | String getEntityTableName>() => Query.getEntity().tableName; 7 | 8 | String getEntityPrimaryKey>() => Query.getEntity().primaryKey.columnName; 9 | 10 | typedef EntityInstanceReflector> = Object? Function(T instance, Symbol field); 11 | 12 | typedef EntityInstanceBuilder = T Function(Map args); 13 | 14 | abstract class EntityMirror { 15 | final T instance; 16 | const EntityMirror(this.instance); 17 | 18 | Object? get(Symbol field); 19 | } 20 | 21 | class Binding, Related extends Entity> extends bindTo { 22 | EntityTypeDefinition get referenceTypeDef => Query.getEntity(); 23 | DBEntityField get reference => referenceTypeDef.columns.firstWhere((e) => e.dartName == on!); 24 | const Binding({required Symbol super.on, super.onDelete, super.onUpdate}) : super(Related); 25 | } 26 | 27 | final class EntityTypeDefinition> { 28 | Type get dartType => T; 29 | 30 | final String tableName; 31 | final List columns; 32 | 33 | final Map> bindings; 34 | 35 | final EntityInstanceReflector mirror; 36 | final EntityInstanceBuilder builder; 37 | 38 | final List converters; 39 | 40 | final bool timestampsEnabled; 41 | 42 | PrimaryKeyField get primaryKey => columns.firstWhereOrNull((e) => e is PrimaryKeyField) as PrimaryKeyField; 43 | 44 | CreatedAtField? get createdAtField => 45 | !timestampsEnabled ? null : columns.firstWhereOrNull((e) => e is CreatedAtField) as CreatedAtField?; 46 | 47 | UpdatedAtField? get updatedAtField => 48 | !timestampsEnabled ? null : columns.firstWhereOrNull((e) => e is UpdatedAtField) as UpdatedAtField?; 49 | 50 | Iterable get fieldsRequiredForCreate => 51 | primaryKey.autoIncrement ? columns.where((e) => e != primaryKey) : columns; 52 | 53 | Iterable get editableColumns => columns.where((e) => e != primaryKey); 54 | 55 | const EntityTypeDefinition( 56 | this.tableName, { 57 | this.columns = const [], 58 | this.bindings = const {}, 59 | required this.mirror, 60 | required this.builder, 61 | this.timestampsEnabled = false, 62 | this.converters = const [], 63 | }); 64 | } 65 | 66 | final class DBEntityField { 67 | /// dart name for property on Entity class 68 | final Symbol dartName; 69 | 70 | /// Column name in the database 71 | final String columnName; 72 | 73 | /// Dart primitive type 74 | final Type type; 75 | 76 | final bool nullable; 77 | final bool unique; 78 | 79 | bool get isPrimaryKey => false; 80 | 81 | const DBEntityField( 82 | this.columnName, 83 | this.type, 84 | this.dartName, { 85 | this.nullable = false, 86 | this.unique = false, 87 | }); 88 | 89 | static PrimaryKeyField primaryKey( 90 | String columnName, 91 | Type type, 92 | Symbol dartName, { 93 | bool? autoIncrement, 94 | }) { 95 | return PrimaryKeyField._( 96 | columnName, 97 | type, 98 | dartName, 99 | autoIncrement: autoIncrement ?? false, 100 | ); 101 | } 102 | 103 | static CreatedAtField createdAt(String columnName, Symbol dartName) { 104 | return CreatedAtField._(columnName, dartName); 105 | } 106 | 107 | static UpdatedAtField updatedAt(String columnName, Symbol dartName) { 108 | return UpdatedAtField._(columnName, dartName); 109 | } 110 | } 111 | 112 | final class PrimaryKeyField extends DBEntityField { 113 | final bool autoIncrement; 114 | 115 | const PrimaryKeyField._( 116 | super.columnName, 117 | super.type, 118 | super.dartName, { 119 | this.autoIncrement = true, 120 | }) : super(nullable: false); 121 | 122 | @override 123 | bool get isPrimaryKey => true; 124 | } 125 | 126 | final class CreatedAtField extends DBEntityField { 127 | const CreatedAtField._(String columnName, Symbol dartName) : super(columnName, DateTime, dartName); 128 | } 129 | 130 | final class UpdatedAtField extends DBEntityField { 131 | const UpdatedAtField._(String columnName, Symbol dartName) : super(columnName, DateTime, dartName); 132 | } 133 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | String symbolToString(Symbol symbol) { 2 | final symbolAsString = symbol.toString(); 3 | return symbolAsString.substring(8, symbolAsString.length - 2); 4 | } 5 | -------------------------------------------------------------------------------- /lib/yaroorm.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'src/database/entity/entity.dart'; 4 | 5 | export 'src/query/query.dart'; 6 | export 'src/database/driver/driver.dart'; 7 | export 'src/database/entity/entity.dart' hide entityMapToDbData, entityToDbData, dbDataToEntity, combineConverters; 8 | export 'src/database/database.dart'; 9 | export 'src/config.dart'; 10 | export 'src/reflection.dart' 11 | hide getEntityTableName, getEntityPrimaryKey, EntityInstanceReflector, EntityInstanceBuilder; 12 | export 'src/migration.dart'; 13 | 14 | const primaryKey = PrimaryKey(autoIncrement: true); 15 | const table = Table(); 16 | const createdAtCol = CreatedAtColumn(); 17 | const updatedAtCol = UpdatedAtColumn(); 18 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: yaroorm 2 | 3 | packages: 4 | - '.' 5 | - packages/** -------------------------------------------------------------------------------- /melos_yaroorm.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/README.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | TODO: Put a short description of the package here that helps potential users 15 | know whether this package might be useful for them. 16 | 17 | ## Features 18 | 19 | TODO: List what your package can do. Maybe include images, gifs, or videos. 20 | 21 | ## Getting started 22 | 23 | TODO: List prerequisites and provide or point to information on how to 24 | start using the package. 25 | 26 | ## Usage 27 | 28 | TODO: Include short and useful examples for package users. Add longer examples 29 | to `/example` folder. 30 | 31 | ```dart 32 | const like = 'sample'; 33 | ``` 34 | 35 | ## Additional information 36 | 37 | TODO: Tell users more about the package: where to find more information, how to 38 | contribute to the package, how to file issues, what response they can expect 39 | from the package authors, and more. 40 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/bin/yaroorm_cli.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:yaroorm_cli/src/builder/utils.dart'; 3 | import 'package:yaroorm_cli/src/commands/init_orm_command.dart'; 4 | import 'package:yaroorm_cli/src/commands/migrate_command.dart'; 5 | import 'package:yaroorm_cli/src/commands/migrate_fresh_command.dart'; 6 | import 'package:yaroorm_cli/src/commands/migrate_reset_command.dart'; 7 | import 'package:yaroorm_cli/src/commands/migrate_rollback_command.dart'; 8 | import 'package:yaroorm_cli/yaroorm_cli.dart'; 9 | 10 | const _commandsToRunInProxyClient = [ 11 | MigrateCommand.commandName, 12 | MigrateFreshCommand.commandName, 13 | MigrationRollbackCommand.commandName, 14 | MigrationResetCommand.commandName, 15 | ]; 16 | 17 | void main(List args) async { 18 | final workingDir = Directory.current; 19 | OrmCLIRunner.resolvedProjectCache = await resolveMigrationAndEntitiesInDir(workingDir); 20 | 21 | final isACommandForProxy = args.isNotEmpty && _commandsToRunInProxyClient.contains(args[0]); 22 | if (!isACommandForProxy) return OrmCLIRunner.start(args); 23 | 24 | final (_, makeSnapshot) = await ( 25 | ensureMigratorFile(), 26 | invalidateKernelSnapshotIfNecessary(), 27 | ).wait; 28 | 29 | late Process process; 30 | 31 | if (kernelFile.existsSync()) { 32 | process = await Process.start('dart', ['run', kernelFile.path, ...args]); 33 | } else { 34 | final tasks = Function()>[ 35 | () async => process = await Process.start('dart', ['run', migratorFile, ...args]), 36 | if (makeSnapshot) () => Process.run('dart', ['compile', 'kernel', migratorFile, '-o', kernelFile.path]), 37 | ]; 38 | 39 | await tasks.map((e) => e.call()).wait; 40 | } 41 | 42 | stdout.addStream(process.stdout); 43 | stderr.addStream(process.stderr); 44 | 45 | final exitCode = await process.exitCode; 46 | 47 | exit(exitCode); 48 | } 49 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/build.yaml: -------------------------------------------------------------------------------- 1 | builders: 2 | yaroorm_generator: 3 | import: "package:yaroorm_cli/yaroorm_cli.dart" 4 | builder_factories: ["yaroormBuilder"] 5 | build_extensions: { ".dart": [".g.part"] } 6 | auto_apply: dependents 7 | build_to: cache 8 | applies_builders: ["source_gen|combining_builder"] 9 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/builder/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; 5 | import 'package:analyzer/dart/analysis/results.dart'; 6 | import 'package:analyzer/dart/constant/value.dart'; 7 | import 'package:analyzer/dart/element/element.dart'; 8 | import 'package:analyzer/dart/element/nullability_suffix.dart'; 9 | import 'package:analyzer/dart/element/type.dart'; 10 | import 'package:analyzer/file_system/physical_file_system.dart'; 11 | import 'package:collection/collection.dart'; 12 | import 'package:grammer/grammer.dart'; 13 | import 'package:mason_logger/mason_logger.dart'; 14 | import 'package:recase/recase.dart'; 15 | import 'package:source_gen/source_gen.dart'; 16 | 17 | import 'package:yaroorm/yaroorm.dart' as orm; 18 | import 'package:crypto/crypto.dart' show md5; 19 | 20 | import '../commands/init_orm_command.dart'; 21 | import '../yaroorm_cli_base.dart'; 22 | 23 | class YaroormCliException implements Exception { 24 | final String message; 25 | YaroormCliException(this.message) : super(); 26 | 27 | @override 28 | String toString() => 'ORM CLI Error: $message'; 29 | } 30 | 31 | TypeChecker typeChecker(Type type) => TypeChecker.fromRuntime(type); 32 | 33 | extension DartTypeExt on DartType { 34 | bool get isNullable => nullabilitySuffix == NullabilitySuffix.question; 35 | } 36 | 37 | String getFieldDbName(FieldElement element, {DartObject? meta}) { 38 | final elementName = element.name; 39 | meta ??= typeChecker(orm.TableColumn).firstAnnotationOf(element, throwOnUnresolved: false); 40 | if (meta != null) { 41 | return ConstantReader(meta).peek('name')?.stringValue ?? elementName; 42 | } 43 | return elementName; 44 | } 45 | 46 | String getTableName(ClassElement element) { 47 | final meta = typeChecker(orm.Table).firstAnnotationOf(element, throwOnUnresolved: false); 48 | return ConstantReader(meta).peek('name')?.stringValue ?? element.name.snakeCase.toPlural().first.toLowerCase(); 49 | } 50 | 51 | String getTypeDefName(String className) { 52 | return '${className.pascalCase.toLowerCase()}TypeDef'; 53 | } 54 | 55 | final RegExp _migrationTimestampRegex = RegExp(r'(\d{4})_(\d{2})_(\d{2})_(\d{6})'); 56 | 57 | DateTime parseMigrationFileDate(String fileName) { 58 | final match = _migrationTimestampRegex.firstMatch(fileName); 59 | if (match == null) { 60 | throw YaroormCliException('Invalid migration name: -> $fileName'); 61 | } 62 | 63 | final year = match.group(1); 64 | final month = match.group(2); 65 | final day = match.group(3); 66 | final time = match.group(4); 67 | 68 | if (year == null || month == null || day == null || time == null) { 69 | throw YaroormCliException('Invalid migration name: -> $fileName'); 70 | } 71 | 72 | return DateTime( 73 | int.parse(year), 74 | int.parse(month), 75 | int.parse(day), 76 | int.parse(time.substring(0, 2)), 77 | int.parse(time.substring(2, 4)), 78 | int.parse(time.substring(4)), 79 | ); 80 | } 81 | 82 | String getMigrationFileName(String name, DateTime date) { 83 | String twoDigit(int number) => number.toString().padLeft(2, '0'); 84 | return '${date.year}_${twoDigit(date.month)}_${twoDigit(date.day)}_${twoDigit(date.hour)}${twoDigit(date.minute)}${twoDigit(date.second)}_$name'; 85 | } 86 | 87 | typedef ResolvedProject = ({ 88 | List migrations, 89 | List entities, 90 | TopLevelVariableElement dbConfig, 91 | }); 92 | 93 | Future resolveMigrationAndEntitiesInDir(Directory workingDir) async { 94 | final collection = AnalysisContextCollection( 95 | includedPaths: [workingDir.absolute.path], 96 | resourceProvider: PhysicalResourceProvider.INSTANCE, 97 | ); 98 | 99 | final List migrations = []; 100 | final List entities = []; 101 | 102 | TopLevelVariableElement? dbConfig; 103 | 104 | await for (final (library, _, _) in _libraries(collection)) { 105 | /// Resolve ORM config 106 | final configIsInitiallyNull = dbConfig == null; 107 | final config = library.element.topLevelElements 108 | .firstWhereOrNull((element) => typeChecker(orm.UseORMConfig).hasAnnotationOfExact(element)); 109 | if (config != null) { 110 | if (configIsInitiallyNull) { 111 | if (config is! TopLevelVariableElement || config.isPrivate) { 112 | throw YaroormCliException('ORM config has to be a top level public variable'); 113 | } 114 | 115 | dbConfig = config; 116 | } else { 117 | throw YaroormCliException('Found more than one ORM Config'); 118 | } 119 | } 120 | 121 | final result = _validateLibrary(library, library.element.identifier); 122 | if (result == null) continue; 123 | 124 | if (result.migrations != null) { 125 | migrations.add(result.migrations!); 126 | } 127 | 128 | if (result.entityClasses != null) { 129 | entities.add(result.entityClasses!); 130 | } 131 | } 132 | 133 | if (dbConfig == null) { 134 | throw YaroormCliException('Did you forget to annotate ORM Config with ${cyan.wrap('@DB.useConfig')} ?'); 135 | } 136 | 137 | return (migrations: migrations, entities: entities, dbConfig: dbConfig); 138 | } 139 | 140 | class Item { 141 | final Iterable elements; 142 | final String path; 143 | 144 | const Item(this.elements, this.path); 145 | } 146 | 147 | ({Item? migrations, Item? entityClasses})? _validateLibrary(ResolvedLibraryResult library, String identifier) { 148 | final classElements = library.element.topLevelElements 149 | .where((e) => !e.isPrivate && e is ClassElement && e.supertype != null && !e.isAbstract) 150 | .toList() 151 | .cast(); 152 | 153 | if (classElements.isEmpty) return null; 154 | 155 | final migrationClasses = 156 | classElements.where((element) => typeChecker(orm.Migration).isExactlyType(element.supertype!)); 157 | final entityClasses = 158 | classElements.where((element) => element.mixins.any((mixin) => typeChecker(orm.Entity).isExactlyType(mixin))); 159 | 160 | return ( 161 | migrations: migrationClasses.isEmpty ? null : Item(migrationClasses, identifier), 162 | entityClasses: entityClasses.isEmpty ? null : Item(entityClasses, identifier), 163 | ); 164 | } 165 | 166 | Stream<(ResolvedLibraryResult, String, String)> _libraries(AnalysisContextCollection collection) async* { 167 | for (final context in collection.contexts) { 168 | final analyzedFiles = context.contextRoot.analyzedFiles().toList(); 169 | 170 | final futures = 171 | analyzedFiles.where((path) => path.endsWith('.dart') && !path.endsWith('_test.dart')).map((filePath) async { 172 | final library = await context.currentSession.getResolvedLibrary(filePath); 173 | if (library is! ResolvedLibraryResult) return null; 174 | return (library, filePath, context.contextRoot.root.path); 175 | }); 176 | 177 | for (final result in await Future.wait(futures)) { 178 | if (result != null) { 179 | yield result; 180 | } 181 | } 182 | } 183 | } 184 | 185 | typedef FieldElementAndReader = ({FieldElement field, ConstantReader reader}); 186 | 187 | final class ParsedEntityClass { 188 | final ClassElement element; 189 | final ConstructorElement constructor; 190 | 191 | final String table; 192 | final String className; 193 | 194 | final List allFields; 195 | 196 | /// {current_field_in_class : external entity being referenced and field} 197 | final Map bindings; 198 | 199 | final List getters; 200 | 201 | /// All other properties aside primarykey, updatedAt and createdAt. 202 | final List normalFields; 203 | 204 | final FieldElementAndReader primaryKey; 205 | final FieldElementAndReader? createdAtField, updatedAtField; 206 | 207 | List get hasManyGetters => 208 | getters.where((getter) => typeChecker(orm.HasMany).isExactlyType(getter.type)).toList(); 209 | 210 | List get belongsToGetters => 211 | getters.where((getter) => typeChecker(orm.BelongsTo).isExactlyType(getter.type)).toList(); 212 | 213 | List get hasOneGetters => 214 | getters.where((getter) => typeChecker(orm.HasOne).isExactlyType(getter.type)).toList(); 215 | 216 | bool get hasAutoIncrementingPrimaryKey { 217 | return primaryKey.reader.peek('autoIncrement')!.boolValue; 218 | } 219 | 220 | bool get timestampsEnabled => (createdAtField ?? updatedAtField) != null; 221 | 222 | List get fieldsRequiredForCreate => [ 223 | if (!hasAutoIncrementingPrimaryKey) primaryKey.field, 224 | ...normalFields, 225 | ]; 226 | 227 | const ParsedEntityClass( 228 | this.table, 229 | this.className, 230 | this.element, { 231 | required this.primaryKey, 232 | required this.constructor, 233 | required this.normalFields, 234 | this.bindings = const {}, 235 | this.createdAtField, 236 | this.updatedAtField, 237 | required this.allFields, 238 | this.getters = const [], 239 | }); 240 | 241 | factory ParsedEntityClass.parse(ClassElement clazz, {ConstantReader? reader}) { 242 | final className = clazz.name; 243 | final tableName = getTableName(clazz); 244 | 245 | final fields = switch (clazz.supertype?.element) { 246 | null => clazz.fields.where(_allowedTypes).toList(growable: false), 247 | _ => [...clazz.fields, ...(clazz.supertype!.element as ClassElement).fields] 248 | .where(_allowedTypes) 249 | .toList(growable: false) 250 | }; 251 | 252 | // Validate un-named class constructor 253 | final primaryConstructor = clazz.constructors.firstWhereOrNull((e) => e.name == ""); 254 | if (primaryConstructor == null) { 255 | throw InvalidGenerationSource( 256 | '$className Entity does not have a default constructor', 257 | element: clazz, 258 | ); 259 | } 260 | 261 | final primaryKey = _getFieldAnnotationByType(primaryConstructor, fields, orm.PrimaryKey); 262 | final createdAt = _getFieldAnnotationByType(primaryConstructor, fields, orm.CreatedAtColumn); 263 | final updatedAt = _getFieldAnnotationByType(primaryConstructor, fields, orm.UpdatedAtColumn); 264 | 265 | // Check should have primary key 266 | if (primaryKey == null) { 267 | throw InvalidGenerationSource( 268 | '$className Entity does not have a primary key', 269 | element: clazz, 270 | ); 271 | } 272 | 273 | final fieldNames = fields.map((e) => e.name); 274 | final notAllowedProps = primaryConstructor.children.where((e) => !fieldNames.contains(e.name)); 275 | if (notAllowedProps.isNotEmpty) { 276 | throw InvalidGenerationSource( 277 | 'These props are not allowed in $className Entity default constructor: ${notAllowedProps.join(', ')}', 278 | element: notAllowedProps.first, 279 | ); 280 | } 281 | 282 | final normalFields = fields 283 | .where((e) => ![primaryKey.field, createdAt?.field, updatedAt?.field].contains(e)) 284 | .toList(growable: false); 285 | 286 | final fieldsWithBindings = _getFieldsAndReaders(primaryConstructor, normalFields, orm.bindTo); 287 | 288 | final Map bindings = {}; 289 | 290 | for (final field in fieldsWithBindings) { 291 | final relatedClass = field.reader.peek('type')!.typeValue.element as ClassElement; 292 | final parsedRelatedClass = ParsedEntityClass.parse(relatedClass); 293 | 294 | /// Check the field we're binding onto. If provided, validate that if exists 295 | /// if not, use the related class primary key 296 | final fieldToBind = field.reader.peek('on')?.symbolValue ?? Symbol(parsedRelatedClass.primaryKey.field.name); 297 | final referencedField = parsedRelatedClass.allFields.firstWhereOrNull((e) => Symbol(e.name) == fieldToBind); 298 | if (referencedField == null) { 299 | throw InvalidGenerationSource( 300 | 'Field $fieldToBind used in Binding does not exist on ${parsedRelatedClass.className} Entity', 301 | element: field.field, 302 | ); 303 | } 304 | 305 | if (referencedField.type != field.field.type) { 306 | throw InvalidGenerationSource( 307 | 'Type-mismatch between fields $className.${field.field.name}(${field.field.type}) and ${parsedRelatedClass.className}.${referencedField.name}(${referencedField.type})', 308 | element: field.field, 309 | ); 310 | } 311 | 312 | bindings[Symbol(field.field.name)] = (entity: parsedRelatedClass, field: fieldToBind, reader: field.reader); 313 | } 314 | 315 | return ParsedEntityClass( 316 | tableName, 317 | className, 318 | clazz, 319 | allFields: fields, 320 | bindings: bindings, 321 | normalFields: normalFields, 322 | getters: clazz.fields.where((e) => e.getter?.isSynthetic == false).toList(), 323 | primaryKey: primaryKey, 324 | constructor: primaryConstructor, 325 | createdAtField: createdAt, 326 | updatedAtField: updatedAt, 327 | ); 328 | } 329 | 330 | static bool _allowedTypes(FieldElement field) { 331 | return field.getter?.isSynthetic ?? false; 332 | } 333 | 334 | static FieldElementAndReader? _getFieldAnnotationByType( 335 | ConstructorElement constructor, 336 | List fields, 337 | Type type, 338 | ) { 339 | final checker = typeChecker(type); 340 | 341 | for (final field in fields) { 342 | final fieldInConstructor = constructor.children 343 | .firstWhereOrNull((e) => e.name == field.name && e is SuperFormalParameterElement && e.metadata.isNotEmpty); 344 | 345 | var result = checker.firstAnnotationOf(field, throwOnUnresolved: false); 346 | 347 | if (result == null && fieldInConstructor != null) { 348 | result = checker.firstAnnotationOf(fieldInConstructor); 349 | } 350 | 351 | if (result != null) { 352 | return (field: field, reader: ConstantReader(result)); 353 | } 354 | } 355 | return null; 356 | } 357 | 358 | static Iterable _getFieldsAndReaders( 359 | ConstructorElement constructor, 360 | List fields, 361 | Type type, 362 | ) sync* { 363 | final checker = typeChecker(type); 364 | 365 | for (final field in fields) { 366 | final fieldInConstructor = constructor.children 367 | .firstWhereOrNull((e) => e.name == field.name && e is SuperFormalParameterElement && e.metadata.isNotEmpty); 368 | var result = checker.firstAnnotationOf(field, throwOnUnresolved: false); 369 | 370 | if (result == null && fieldInConstructor != null) { 371 | result = checker.firstAnnotationOf(fieldInConstructor); 372 | } 373 | 374 | if (result == null) continue; 375 | yield (field: field, reader: ConstantReader(result)); 376 | } 377 | } 378 | } 379 | 380 | String symbolToString(Symbol symbol) { 381 | final symbolAsString = symbol.toString(); 382 | return symbolAsString.substring(8, symbolAsString.length - 2); 383 | } 384 | 385 | const _migratorFileContent = ''' 386 | import 'package:yaroorm_cli/yaroorm_cli.dart'; 387 | import '../../database/database.dart'; 388 | 389 | void main(List args) async { 390 | initializeORM(); 391 | 392 | await OrmCLIRunner.start(args); 393 | } 394 | '''; 395 | 396 | Future ensureMigratorFile() async { 397 | final file = File(migratorFile); 398 | if (!file.existsSync()) { 399 | await (file..createSync(recursive: true)).writeAsString(_migratorFileContent); 400 | } 401 | } 402 | 403 | Future invalidateKernelSnapshotIfNecessary() async { 404 | final entitiesMd5 = OrmCLIRunner.resolvedProjectCache.entities 405 | .map((e) => e.elements.map((clazz) => '${clazz.name}: ${_generateMD5ForClassElement(clazz)}')) 406 | .flattened 407 | .join('\n'); 408 | 409 | final existingChecksum = await migratorCheckSumFile.readAsString().safeRun(); 410 | if (existingChecksum == entitiesMd5) return false; 411 | 412 | await [ 413 | kernelFile.delete().safeRun(), 414 | migratorCheckSumFile.writeAsString(entitiesMd5, mode: FileMode.write).safeRun(), 415 | ].wait; 416 | 417 | return true; 418 | } 419 | 420 | String _generateMD5ForClassElement(ClassElement classElement) { 421 | final classInfo = StringBuffer()..writeln('Class: ${classElement.name}'); 422 | for (var field in classElement.fields) { 423 | classInfo.writeln('Field: ${field.type} ${field.name} ${field.type.isNullable}'); 424 | } 425 | 426 | for (var method in classElement.methods) { 427 | classInfo.writeln('Method: ${method.name}'); 428 | } 429 | 430 | for (var metadata in classElement.metadata) { 431 | classInfo.writeln('Metadata: ${metadata.toString()}'); 432 | } 433 | 434 | return md5.convert(utf8.encode(classInfo.toString())).toString(); 435 | } 436 | 437 | extension _SafeCall on Future { 438 | Future safeRun({Function(Object error)? onError}) async { 439 | try { 440 | return await this; 441 | } catch (error) { 442 | onError?.call(error); 443 | } 444 | return null; 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:args/command_runner.dart'; 4 | import 'package:cli_table/cli_table.dart'; 5 | import 'package:collection/collection.dart'; 6 | import 'package:mason_logger/mason_logger.dart'; 7 | import 'package:yaroorm/yaroorm.dart' hide Table; 8 | import '../misc/utils.dart'; 9 | 10 | import '../misc/migration.dart'; 11 | 12 | class MigrationDefn { 13 | late final String name; 14 | late final List up, down; 15 | 16 | MigrationDefn(Migration migration) { 17 | name = migration.name; 18 | up = _accumulate(migration.name, migration.up); 19 | down = _accumulate(migration.name, migration.down); 20 | } 21 | 22 | List _accumulate(String scriptName, void Function(List schemas) func) { 23 | final result = []; 24 | func(result); 25 | return result; 26 | } 27 | } 28 | 29 | final migrationLogTable = Table( 30 | header: ['Migration', 'Status'], 31 | columnWidths: [30, 30], 32 | style: TableStyle(header: ['green']), 33 | ); 34 | 35 | abstract class OrmCommand extends Command { 36 | static const String connectionArg = 'connection'; 37 | 38 | YaroormConfig get ormConfig => DB.config; 39 | 40 | String get migrationTableName => ormConfig.migrationsTable; 41 | 42 | String get dbConnection { 43 | final defaultConn = ormConfig.defaultConnName; 44 | final args = globalResults; 45 | if (args == null) return defaultConn; 46 | return args.wasParsed(OrmCommand.connectionArg) ? args[OrmCommand.connectionArg] : defaultConn; 47 | } 48 | 49 | List get migrationDefinitions { 50 | return (DB.migrations) 51 | .where((e) => e.connection == null || e.connection == dbConnection) 52 | .map(MigrationDefn.new) 53 | .toList(); 54 | } 55 | 56 | @override 57 | FutureOr run() async { 58 | if (ormConfig.connections.firstWhereOrNull((e) => e.name == dbConnection) == null) { 59 | logger.err('No connection named ${cyan.wrap(dbConnection)}'); 60 | ExitCode.software.code; 61 | } 62 | 63 | Query.addTypeDef(migrationentityTypeDef); 64 | 65 | final driver = DB.driver(dbConnection); 66 | await driver.connect(); 67 | 68 | await execute(driver); 69 | 70 | await driver.disconnect(); 71 | return ExitCode.success.code; 72 | } 73 | 74 | Future execute(DatabaseDriver driver); 75 | } 76 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/create_migration.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/command_runner.dart'; 4 | import 'package:code_builder/code_builder.dart'; 5 | import 'package:dart_style/dart_style.dart'; 6 | import 'package:mason_logger/mason_logger.dart'; 7 | 8 | import 'package:path/path.dart' as path; 9 | import 'package:recase/recase.dart'; 10 | import '../builder/utils.dart'; 11 | import 'init_orm_command.dart'; 12 | import '../misc/utils.dart'; 13 | 14 | class CreateMigrationCommand extends Command { 15 | static const String commandName = 'create'; 16 | 17 | @override 18 | String get description => 'create migration file'; 19 | 20 | @override 21 | String get name => commandName; 22 | 23 | @override 24 | Future run() async { 25 | final name = argResults!.arguments.last.snakeCase; 26 | final time = DateTime.now(); 27 | final fileName = getMigrationFileName(name, time); 28 | final directory = Directory.current; 29 | 30 | final progress = logger.progress('Create migration ${green.wrap(fileName)}'); 31 | 32 | final library = Library((library) => library.body.addAll([ 33 | Directive.import('package:yaroorm/yaroorm.dart'), 34 | Class((c) => c 35 | ..name = name.pascalCase 36 | ..extend = refer('Migration') 37 | ..methods.addAll([ 38 | Method.returnsVoid((m) => m 39 | ..name = 'up' 40 | ..annotations.add(CodeExpression(Code('override'))) 41 | ..body = const Code('') 42 | ..requiredParameters.add(Parameter((p) => p 43 | ..name = 'schemas' 44 | ..type = refer('List')))), 45 | Method.returnsVoid((m) => m 46 | ..name = 'down' 47 | ..annotations.add(CodeExpression(Code('override'))) 48 | ..body = const Code('') 49 | ..requiredParameters.add(Parameter((p) => p 50 | ..name = 'schemas' 51 | ..type = refer('List')))) 52 | ])), 53 | ])); 54 | 55 | final file = File(path.join(migrationsDir.path, '$fileName.dart')); 56 | 57 | final emitter = DartEmitter(orderDirectives: true, useNullSafetySyntax: true); 58 | await file.writeAsString(DartFormatter().format(library.accept(emitter).toString().split('\n').join('\n'))); 59 | 60 | final result = await resolveMigrationAndEntitiesInDir(directory); 61 | if (result.migrations.isEmpty) { 62 | progress.fail('Failed to create migration file.'); 63 | return ExitCode.software.code; 64 | } 65 | 66 | await Future.wait([ 67 | if (migratorCheckSumFile.existsSync()) migratorCheckSumFile.delete(), 68 | if (kernelFile.existsSync()) kernelFile.delete(), 69 | initOrmInProject(directory, result.migrations, result.entities, result.dbConfig), 70 | ]); 71 | 72 | progress.complete('Migration file created ✅'); 73 | 74 | return ExitCode.success.code; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/init_orm_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:analyzer/dart/element/element.dart'; 5 | import 'package:args/command_runner.dart'; 6 | import 'package:code_builder/code_builder.dart'; 7 | import 'package:collection/collection.dart'; 8 | import 'package:dart_style/dart_style.dart'; 9 | import 'package:mason_logger/mason_logger.dart'; 10 | 11 | import '../builder/utils.dart'; 12 | import '../misc/utils.dart'; 13 | 14 | import 'package:path/path.dart' as path; 15 | 16 | import '../yaroorm_cli_base.dart'; 17 | 18 | Directory get databaseDir { 19 | final dir = Directory(path.join(Directory.current.path, 'database')); 20 | if (!dir.existsSync()) dir.createSync(); 21 | return dir; 22 | } 23 | 24 | Directory get migrationsDir { 25 | final dir = Directory(path.join(databaseDir.path, 'migrations')); 26 | if (!dir.existsSync()) dir.createSync(); 27 | return dir; 28 | } 29 | 30 | final databaseInitFile = File('${path.join(databaseDir.path, 'database')}.dart'); 31 | 32 | final yaroormDirectory = path.join(Directory.current.path, '.dart_tool', 'yaroorm_cli'); 33 | final kernelFile = File(path.join(yaroormDirectory, 'migrator.dill')); 34 | final migratorCheckSumFile = File(path.join(yaroormDirectory, '.migrator_checksum')); 35 | 36 | final migratorFile = path.join(yaroormDirectory, 'migrator.dart'); 37 | 38 | class InitializeOrmCommand extends Command { 39 | static const String commandName = 'init'; 40 | 41 | @override 42 | String get description => 'Initialize ORM in project'; 43 | 44 | @override 45 | String get name => commandName; 46 | 47 | @override 48 | FutureOr? run() async { 49 | final workingDir = Directory.current; 50 | final progress = logger.progress('Initializing Yaroorm 📦'); 51 | 52 | try { 53 | final result = OrmCLIRunner.resolvedProjectCache; 54 | if (result.migrations.isEmpty) { 55 | progress.fail('Yaroorm 📦 not initialized. No migrations found.'); 56 | return ExitCode.software.code; 57 | } 58 | 59 | await initOrmInProject(workingDir, result.migrations, result.entities, result.dbConfig); 60 | 61 | progress.complete('Yaroorm 📦 initialized 🚀'); 62 | 63 | return ExitCode.success.code; 64 | } on YaroormCliException catch (e) { 65 | progress.fail('🗙 ORM initialize step failed'); 66 | logger.err(e.toString()); 67 | exit(ExitCode.software.code); 68 | } 69 | } 70 | } 71 | 72 | Future initOrmInProject( 73 | Directory workingDir, 74 | List migrations, 75 | List entities, 76 | TopLevelVariableElement dbConfig, 77 | ) async { 78 | final entityNames = entities.map((e) => e.elements.map((e) => e.name)).fold({}, (preV, e) => preV..addAll(e)); 79 | final databaseFile = File(path.join(databaseDir.path, 'database.dart')); 80 | 81 | final fsPath = path.relative(dbConfig.library.identifier.replaceFirst('file://', '').trim()); 82 | final configPath = fsPath.startsWith('package:') ? fsPath : path.relative(fsPath, from: databaseDir.path); 83 | 84 | final migrationFileNameDateMap = migrations 85 | .map((e) => path.basename(e.path)) 86 | .fold({}, (preV, filename) => preV..[filename] = parseMigrationFileDate(filename)); 87 | 88 | final sortedMigrationsList = (migrations 89 | ..sort((a, b) => migrationFileNameDateMap[path.basename(a.path)]!.compareTo( 90 | migrationFileNameDateMap[path.basename(b.path)]!, 91 | ))) 92 | .mapIndexed((index, element) => (index: index, element: element)); 93 | 94 | final addMigrationsToDbCode = ''' 95 | /// Configure Migrations Order 96 | DB.migrations.addAll([ 97 | ${sortedMigrationsList.map((mig) => mig.element.elements.map((classElement) => '_m${mig.index}.${classElement.name}()').join(', ')).join(', ')}, 98 | ]); 99 | '''; 100 | 101 | final library = Library((p0) => p0 102 | ..comments.add('GENERATED CODE - DO NOT MODIFY BY HAND') 103 | ..directives.addAll([ 104 | Directive.import('package:yaroorm/yaroorm.dart'), 105 | ...entities.map((e) => e.path).toSet().map((e) => Directive.import(e)), 106 | Directive.import(configPath, as: 'config'), 107 | ...sortedMigrationsList 108 | .map((e) => Directive.import('migrations/${path.basename(e.element.path)}', as: '_m${e.index}')) 109 | ]) 110 | ..body.add(Method.returnsVoid((m) => m 111 | ..name = 'initializeORM' 112 | ..body = Code(''' 113 | /// Add Type Definitions to Query Runner 114 | ${entityNames.map((name) => 'Query.addTypeDef<$name>(${getTypeDefName(name)});').join('\n')} 115 | 116 | ${sortedMigrationsList.isNotEmpty ? addMigrationsToDbCode : ''} 117 | 118 | DB.init(config.${dbConfig.name}); 119 | ''')))); 120 | 121 | final emitter = DartEmitter.scoped(orderDirectives: true, useNullSafetySyntax: true); 122 | 123 | final code = DartFormatter().format([library.accept(emitter)].join('\n\n')); 124 | 125 | await databaseFile.writeAsString(code); 126 | } 127 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/migrate_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:yaroorm/yaroorm.dart'; 4 | 5 | import '../misc/utils.dart'; 6 | import '../misc/migration.dart'; 7 | import 'command.dart'; 8 | 9 | class MigrateCommand extends OrmCommand { 10 | static const String commandName = 'migrate'; 11 | 12 | @override 13 | String get description => 'migrate your database'; 14 | 15 | @override 16 | String get name => commandName; 17 | 18 | @override 19 | Future execute(DatabaseDriver driver, {bool writeLogs = true}) async { 20 | await ensureMigrationsTableReady(driver); 21 | 22 | final lastBatchNumber = await getLastBatchNumber(driver, migrationTableName); 23 | final batchNos = lastBatchNumber + 1; 24 | 25 | for (final migration in migrationDefinitions) { 26 | final fileName = migration.name; 27 | 28 | if (await hasAlreadyMigratedScript(fileName, driver)) { 29 | migrationLogTable.add([fileName, '〽️ already migrated']); 30 | continue; 31 | } 32 | 33 | await driver.transaction((txnDriver) async { 34 | for (final schema in migration.up) { 35 | final sql = schema.toScript(driver.blueprint); 36 | await txnDriver.execute(sql); 37 | } 38 | 39 | await MigrationEntityQuery.driver(txnDriver).insert(NewMigrationEntity( 40 | migration: fileName, 41 | batch: batchNos, 42 | )); 43 | 44 | migrationLogTable.add([fileName, '✅ migrated']); 45 | }); 46 | } 47 | 48 | if (writeLogs) { 49 | logger.write(migrationLogTable.toString()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/migrate_fresh_command.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaroorm/yaroorm.dart'; 2 | 3 | import '../misc/utils.dart'; 4 | 5 | import 'command.dart'; 6 | import 'migrate_command.dart'; 7 | import 'migrate_reset_command.dart'; 8 | 9 | class MigrateFreshCommand extends OrmCommand { 10 | static const String commandName = 'migrate:fresh'; 11 | 12 | @override 13 | String get name => commandName; 14 | 15 | @override 16 | String get description => 'reset and re-run all database migrations'; 17 | 18 | @override 19 | Future execute(DatabaseDriver driver) async { 20 | await MigrationResetCommand().execute(driver, writeLogs: false); 21 | await MigrateCommand().execute(driver, writeLogs: false); 22 | logger.write(migrationLogTable.toString()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/migrate_reset_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:yaroorm/yaroorm.dart'; 5 | import '../misc/utils.dart'; 6 | 7 | import '../misc/migration.dart'; 8 | import 'command.dart'; 9 | import 'migrate_rollback_command.dart'; 10 | 11 | class MigrationResetCommand extends OrmCommand { 12 | static const String commandName = 'migrate:reset'; 13 | 14 | @override 15 | String get description => 'reset database migrations'; 16 | 17 | @override 18 | String get name => commandName; 19 | 20 | @override 21 | Future execute(DatabaseDriver driver, {bool writeLogs = true}) async { 22 | await ensureMigrationsTableReady(driver); 23 | 24 | final migrationsList = await MigrationEntityQuery.driver(driver).findMany( 25 | orderBy: [OrderMigrationEntityBy.batch(order: OrderDirection.desc)], 26 | ); 27 | if (migrationsList.isEmpty) { 28 | print('𐄂 skipped: reason: no migrations to reset'); 29 | return; 30 | } 31 | 32 | final rollbacks = migrationDefinitions.reversed.map((e) { 33 | final entry = migrationsList.firstWhereOrNull((entry) => e.name == entry.migration); 34 | return entry == null ? null : (entry: entry, schemas: e.down); 35 | }).nonNulls; 36 | 37 | await processRollbacks(driver, rollbacks, table: migrationLogTable); 38 | 39 | if (writeLogs) { 40 | logger.write(migrationLogTable.toString()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/commands/migrate_rollback_command.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cli_table/cli_table.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:yaroorm/yaroorm.dart' hide Table; 6 | 7 | import '../misc/utils.dart'; 8 | import '../misc/migration.dart'; 9 | 10 | import 'command.dart'; 11 | 12 | class MigrationRollbackCommand extends OrmCommand { 13 | static const String commandName = 'migrate:rollback'; 14 | 15 | @override 16 | String get description => 'rollback last migration batch'; 17 | 18 | @override 19 | String get name => commandName; 20 | 21 | @override 22 | Future execute(DatabaseDriver driver) async { 23 | await ensureMigrationsTableReady(driver); 24 | 25 | final lastBatchNumber = await getLastBatchNumber(driver, migrationTableName); 26 | 27 | final entries = await MigrationEntityQuery.driver(driver) 28 | .where( 29 | (migration) => migration.batch(lastBatchNumber), 30 | ) 31 | .findMany(); 32 | 33 | /// rollbacks start from the last class listed in the migrations list 34 | final migrationTask = migrationDefinitions 35 | .map((defn) { 36 | final entry = entries.firstWhereOrNull((e) => e.migration == defn.name); 37 | return entry == null ? null : (entry: entry, schemas: defn.down); 38 | }) 39 | .nonNulls 40 | .lastOrNull; 41 | 42 | if (migrationTask == null) { 43 | print('𐄂 skipped: reason: no migration to rollback'); 44 | return; 45 | } 46 | 47 | await processRollbacks(driver, [migrationTask], table: migrationLogTable); 48 | 49 | logger.write(migrationLogTable.toString()); 50 | } 51 | } 52 | 53 | typedef Rollback = ({MigrationEntity entry, List schemas}); 54 | 55 | Future processRollbacks( 56 | DatabaseDriver driver, 57 | Iterable rollbacks, { 58 | Table? table, 59 | }) async { 60 | for (final rollback in rollbacks) { 61 | await driver.transaction((transactor) async { 62 | for (var e in rollback.schemas) { 63 | await transactor.execute(e.toScript(driver.blueprint)); 64 | } 65 | 66 | await MigrationEntityQuery.driver(transactor) 67 | .where( 68 | (migration) => migration.id(rollback.entry.id), 69 | ) 70 | .delete(); 71 | }); 72 | 73 | table?.add([rollback.entry.migration, '✅ rolled back']); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/misc/migration.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names 2 | 3 | import 'package:yaroorm/yaroorm.dart'; 4 | 5 | part 'migration.g.dart'; 6 | 7 | @Table(name: 'migrations') 8 | class MigrationEntity with Entity { 9 | @primaryKey 10 | final int id; 11 | 12 | final String migration; 13 | 14 | final int batch; 15 | 16 | MigrationEntity(this.id, this.migration, this.batch) { 17 | super.initialize(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/misc/migration.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'migration.dart'; 4 | 5 | // ************************************************************************** 6 | // EntityGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names 10 | 11 | Query get MigrationEntityQuery => DB.query(); 12 | CreateSchema get MigrationEntitySchema => Schema.fromEntity(); 13 | EntityTypeDefinition get migrationentityTypeDef => EntityTypeDefinition( 14 | "migrations", 15 | timestampsEnabled: false, 16 | columns: [ 17 | DBEntityField.primaryKey("id", int, #id, autoIncrement: true), 18 | DBEntityField("migration", String, #migration), 19 | DBEntityField("batch", int, #batch) 20 | ], 21 | mirror: (instance, field) => switch (field) { 22 | #id => instance.id, 23 | #migration => instance.migration, 24 | #batch => instance.batch, 25 | _ => throw Exception('Unknown property $field'), 26 | }, 27 | builder: (args) => MigrationEntity( 28 | args[#id], 29 | args[#migration], 30 | args[#batch], 31 | ), 32 | ); 33 | 34 | class OrderMigrationEntityBy extends OrderBy { 35 | const OrderMigrationEntityBy.migration({OrderDirection order = OrderDirection.asc}) : super("migration", order); 36 | 37 | const OrderMigrationEntityBy.batch({OrderDirection order = OrderDirection.asc}) : super("batch", order); 38 | } 39 | 40 | class NewMigrationEntity extends CreateEntity { 41 | const NewMigrationEntity({ 42 | required this.migration, 43 | required this.batch, 44 | }); 45 | 46 | final String migration; 47 | 48 | final int batch; 49 | 50 | @override 51 | Map get toMap => {#migration: migration, #batch: batch}; 52 | } 53 | 54 | class UpdateMigrationEntity extends UpdateEntity { 55 | UpdateMigrationEntity({ 56 | this.migration = const Value.absent(), 57 | this.batch = const Value.absent(), 58 | }); 59 | 60 | final Value migration; 61 | 62 | final Value batch; 63 | 64 | @override 65 | Map get toMap => { 66 | if (migration.present) #migration: migration.value, 67 | if (batch.present) #batch: batch.value, 68 | }; 69 | } 70 | 71 | extension MigrationEntityWhereBuilderExtension on WhereClauseBuilder { 72 | WhereClauseValue id(int value) => $equal("id", value); 73 | WhereClauseValue migration(String value) => $equal("migration", value); 74 | WhereClauseValue batch(int value) => $equal("batch", value); 75 | } 76 | 77 | extension MigrationEntityWhereHelperExtension on Query { 78 | Future findById(int val) => findOne(where: (migrationentity) => migrationentity.id(val)); 79 | Future findByMigration(String val) => 80 | findOne(where: (migrationentity) => migrationentity.migration(val)); 81 | Future findByBatch(int val) => findOne(where: (migrationentity) => migrationentity.batch(val)); 82 | } 83 | 84 | extension MigrationEntityRelationsBuilder on JoinBuilder {} 85 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/misc/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:mason_logger/mason_logger.dart'; 4 | import 'package:yaroorm/yaroorm.dart'; 5 | import 'migration.dart'; 6 | 7 | final logger = Logger(); 8 | 9 | Future ensureMigrationsTableReady(DatabaseDriver driver) async { 10 | final hasTable = await driver.hasTable(DB.config.migrationsTable); 11 | if (hasTable) return; 12 | 13 | final script = MigrationEntitySchema.toScript(driver.blueprint); 14 | await driver.execute(script); 15 | } 16 | 17 | Future hasAlreadyMigratedScript( 18 | String scriptName, 19 | DatabaseDriver driver, 20 | ) async { 21 | final result = await MigrationEntityQuery.driver(driver).findByMigration(scriptName); 22 | return result != null; 23 | } 24 | 25 | Future getLastBatchNumber( 26 | DatabaseDriver driver, 27 | String migrationsTable, 28 | ) async { 29 | final result = await MigrationEntityQuery.driver(driver).max('batch'); 30 | return result.toInt(); 31 | } 32 | 33 | /// Flushes the stdout and stderr streams, then exits the program with the given 34 | /// status code. 35 | /// 36 | /// This returns a Future that will never complete, since the program will have 37 | /// exited already. This is useful to prevent Future chains from proceeding 38 | /// after you've decided to exit. 39 | Future flushThenExit(int status) { 40 | return Future.wait([stdout.close(), stderr.close()]).then((_) => exit(status)); 41 | } 42 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/src/yaroorm_cli_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:cli_completion/cli_completion.dart'; 2 | import 'package:mason_logger/mason_logger.dart'; 3 | import 'package:args/command_runner.dart'; 4 | 5 | import 'builder/utils.dart'; 6 | import 'commands/command.dart'; 7 | import 'commands/create_migration.dart'; 8 | import 'commands/init_orm_command.dart'; 9 | import 'commands/migrate_command.dart'; 10 | import 'commands/migrate_fresh_command.dart'; 11 | import 'commands/migrate_reset_command.dart'; 12 | import 'commands/migrate_rollback_command.dart'; 13 | import 'misc/utils.dart'; 14 | 15 | const executableName = 'yaroorm'; 16 | const description = 'yaroorm command-line tool'; 17 | 18 | class OrmCLIRunner extends CompletionCommandRunner { 19 | static late ResolvedProject resolvedProjectCache; 20 | 21 | static Future start(List args) async { 22 | run() async { 23 | try { 24 | return (await OrmCLIRunner._().run(args)) ?? 0; 25 | } on UsageException catch (error) { 26 | print(error.toString()); 27 | return ExitCode.software.code; 28 | } 29 | } 30 | 31 | return flushThenExit(await run()); 32 | } 33 | 34 | OrmCLIRunner._() : super(executableName, description) { 35 | argParser.addOption( 36 | OrmCommand.connectionArg, 37 | abbr: 'c', 38 | help: 'specify database connection', 39 | ); 40 | 41 | addCommand(InitializeOrmCommand()); 42 | addCommand(CreateMigrationCommand()); 43 | 44 | addCommand(MigrateCommand()); 45 | addCommand(MigrateFreshCommand()); 46 | addCommand(MigrationRollbackCommand()); 47 | addCommand(MigrationResetCommand()); 48 | } 49 | 50 | @override 51 | void printUsage() => logger.info(usage); 52 | } 53 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/lib/yaroorm_cli.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'package:build/build.dart'; 4 | 5 | import 'src/builder/generator.dart'; 6 | 7 | export 'src/yaroorm_cli_base.dart'; 8 | 9 | /// Builds generators for `build_runner` to run 10 | Builder yaroormBuilder(BuilderOptions options) => generatorFactoryBuilder(options); 11 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/melos_yaroorm_cli.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: yaroorm_cli 2 | description: A starting point for Dart libraries or applications. 3 | version: 0.0.3+1 4 | homepage: https://docs.yaroo.dev/orm/quickstart 5 | repository: https://github.com/codekeyz/yaroo 6 | 7 | environment: 8 | sdk: ^3.3.1 9 | 10 | dependencies: 11 | # Code-Gen 12 | analyzer: ^6.8.0 13 | code_builder: ^4.10.0 14 | source_gen: ^1.5.0 15 | dart_style: ^2.3.0 16 | build: ^2.3.1 17 | 18 | # CLI 19 | args: ^2.4.2 20 | path: ^1.9.0 21 | crypto: ^3.0.3 22 | recase: ^4.1.0 23 | grammer: ^1.0.3 24 | yaroorm: 25 | git: 26 | url: 'https://github.com/codekeyz/yaroorm.git' 27 | cli_table: ^1.0.2 28 | collection: ^1.19.1 29 | mason_logger: ^0.3.1 30 | cli_completion: ^0.5.1 31 | 32 | dev_dependencies: 33 | lints: ^4.0.0 34 | test: ^1.24.0 35 | 36 | executables: 37 | yaroorm: yaroorm_cli 38 | -------------------------------------------------------------------------------- /packages/yaroorm_cli/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: yaroorm 2 | dependency_overrides: 3 | yaroorm: 4 | path: ../.. 5 | -------------------------------------------------------------------------------- /packages/yaroorm_test/.gitignore: -------------------------------------------------------------------------------- 1 | database/database.dart 2 | -------------------------------------------------------------------------------- /packages/yaroorm_test/database/migrations/2024_04_21_003612_create_users_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaroorm/yaroorm.dart'; 2 | import 'package:yaroorm_test/src/models.dart'; 3 | 4 | class AddUsersTable extends Migration { 5 | @override 6 | void up(List schemas) { 7 | schemas.add(UserSchema); 8 | } 9 | 10 | @override 11 | void down(List schemas) { 12 | schemas.add(Schema.dropIfExists(UserSchema)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/yaroorm_test/database/migrations/2024_04_21_003650_create_posts_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaroorm/yaroorm.dart'; 2 | import 'package:yaroorm_test/src/models.dart'; 3 | 4 | class AddPostsTable extends Migration { 5 | @override 6 | void up(List schemas) { 7 | schemas.addAll([PostSchema, PostCommentSchema]); 8 | } 9 | 10 | @override 11 | void down(List schemas) { 12 | schemas.add(Schema.dropIfExists(PostCommentSchema)); 13 | 14 | schemas.add(Schema.dropIfExists(PostSchema)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/yaroorm_test/lib/db_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart' as path; 2 | import 'package:yaroorm/yaroorm.dart'; 3 | 4 | @DB.useConfig 5 | final config = YaroormConfig( 6 | 'foo_sqlite', 7 | connections: [ 8 | DatabaseConnection( 9 | 'foo_sqlite', 10 | DatabaseDriverType.sqlite, 11 | database: path.absolute('database', 'db.sqlite'), 12 | dbForeignKeys: true, 13 | ), 14 | DatabaseConnection( 15 | 'bar_mariadb', 16 | DatabaseDriverType.mariadb, 17 | database: 'test_db', 18 | host: 'localhost', 19 | username: 'tester', 20 | password: 'password', 21 | port: 3000, 22 | ), 23 | DatabaseConnection( 24 | 'moo_mysql', 25 | DatabaseDriverType.mysql, 26 | database: 'test_db', 27 | host: 'localhost', 28 | username: 'tester', 29 | password: 'password', 30 | port: 3001, 31 | secure: true, 32 | ), 33 | DatabaseConnection( 34 | 'foo_pgsql', 35 | DatabaseDriverType.pgsql, 36 | database: 'test_db', 37 | host: 'localhost', 38 | username: 'tester', 39 | password: 'password', 40 | port: 3002, 41 | ), 42 | ], 43 | ); 44 | -------------------------------------------------------------------------------- /packages/yaroorm_test/lib/src/models.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaroorm/yaroorm.dart'; 2 | 3 | part 'models.g.dart'; 4 | 5 | @table 6 | class User with Entity { 7 | @primaryKey 8 | final int id; 9 | 10 | final String firstname; 11 | final String lastname; 12 | final int age; 13 | 14 | @TableColumn(name: 'home_address') 15 | final String homeAddress; 16 | 17 | User({ 18 | required this.id, 19 | required this.firstname, 20 | required this.lastname, 21 | required this.age, 22 | required this.homeAddress, 23 | }) { 24 | super.initialize(); 25 | } 26 | 27 | HasMany get posts => hasMany(#posts); 28 | } 29 | 30 | @Table(name: 'posts') 31 | class Post with Entity { 32 | @primaryKey 33 | final int id; 34 | 35 | final String title; 36 | final String description; 37 | 38 | final String? imageUrl; 39 | 40 | @bindTo(User, onUpdate: ForeignKeyAction.cascade, onDelete: ForeignKeyAction.cascade) 41 | final int userId; 42 | 43 | @createdAtCol 44 | final DateTime createdAt; 45 | 46 | @updatedAtCol 47 | final DateTime updatedAt; 48 | 49 | Post( 50 | this.id, 51 | this.title, 52 | this.description, { 53 | this.imageUrl, 54 | required this.userId, 55 | required this.createdAt, 56 | required this.updatedAt, 57 | }) { 58 | super.initialize(); 59 | } 60 | 61 | HasMany get comments => hasMany(#comments); 62 | 63 | BelongsTo get owner => belongsTo(#owner); 64 | } 65 | 66 | @table 67 | class PostComment with Entity { 68 | @PrimaryKey(autoIncrement: false) 69 | final String id; 70 | 71 | final String comment; 72 | 73 | @bindTo(Post, onDelete: ForeignKeyAction.cascade) 74 | final int postId; 75 | 76 | PostComment(this.id, this.comment, {required this.postId}) { 77 | super.initialize(); 78 | } 79 | 80 | Map toJson() => { 81 | 'id': id, 82 | 'comment': comment, 83 | 'postId': postId, 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /packages/yaroorm_test/lib/test_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:yaroorm_test/src/models.dart'; 2 | 3 | final usersList = [ 4 | /// Ghana Users - [6] 5 | NewUser( 6 | firstname: 'Kofi', 7 | lastname: 'Duke', 8 | age: 22, 9 | homeAddress: "Accra, Ghana", 10 | ), 11 | NewUser( 12 | firstname: 'Foo', 13 | lastname: 'Bar', 14 | age: 23, 15 | homeAddress: "Kumasi, Ghana", 16 | ), 17 | NewUser( 18 | firstname: 'Bar', 19 | lastname: 'Moo', 20 | age: 24, 21 | homeAddress: "Cape Coast, Ghana", 22 | ), 23 | NewUser( 24 | firstname: 'Kee', 25 | lastname: 'Koo', 26 | age: 25, 27 | homeAddress: "Accra, Ghana", 28 | ), 29 | NewUser( 30 | firstname: 'Poo', 31 | lastname: 'Paa', 32 | age: 26, 33 | homeAddress: "Accra, Ghana", 34 | ), 35 | NewUser( 36 | firstname: 'Merh', 37 | lastname: 'Moor', 38 | age: 27, 39 | homeAddress: "Accra, Ghana", 40 | ), 41 | 42 | /// Nigerian Users - [22] 43 | NewUser(firstname: 'Abdul', lastname: 'Ibrahim', age: 28, homeAddress: "Owerri, Nigeria"), 44 | NewUser(firstname: 'Amina', lastname: 'Sule', age: 29, homeAddress: "Owerri, Nigeria"), 45 | NewUser(firstname: 'Chukwudi', lastname: 'Okafor', age: 30, homeAddress: "Lagos, Nigeria"), 46 | NewUser(firstname: 'Chioma', lastname: 'Nwosu', age: 31, homeAddress: "Lagos, Nigeria"), 47 | NewUser(firstname: 'Yusuf', lastname: 'Aliyu', age: 32, homeAddress: "Owerri, Nigeria"), 48 | NewUser(firstname: 'Blessing', lastname: 'Okonkwo', age: 33, homeAddress: "Owerri, Nigeria"), 49 | NewUser(firstname: 'Tunde', lastname: 'Williams', age: 34, homeAddress: "Abuja, Nigeria"), 50 | NewUser(firstname: 'Rukayat', lastname: 'Sanni', age: 35, homeAddress: "Abuja, Nigeria"), 51 | NewUser(firstname: 'Segun', lastname: 'Adeleke', age: 36, homeAddress: "Lagos, Nigeria"), 52 | NewUser(firstname: 'Abdullahi', lastname: 'Mohammed', age: 46, homeAddress: "Lagos, Nigeria"), 53 | NewUser(firstname: 'Chidinma', lastname: 'Onyeka', age: 47, homeAddress: "Owerri, Nigeria"), 54 | NewUser(firstname: 'Bola', lastname: 'Akinwumi', age: 48, homeAddress: "Owerri, Nigeria"), 55 | NewUser(firstname: 'Haruna', lastname: 'Bello', age: 49, homeAddress: "Lagos, Nigeria"), 56 | NewUser(firstname: 'Habiba', lastname: 'Yusuf', age: 50, homeAddress: "Owerri, Nigeria"), 57 | NewUser(firstname: 'Tochukwu', lastname: 'Eze', age: 50, homeAddress: "Lagos, Nigeria"), 58 | NewUser(firstname: 'Ade', lastname: 'Ogunbanjo', age: 50, homeAddress: "Owerri, Nigeria"), 59 | NewUser(firstname: 'Zainab', lastname: 'Abubakar', age: 50, homeAddress: "Lagos, Nigeria"), 60 | NewUser(firstname: 'Chijioke', lastname: 'Nwachukwu', age: 54, homeAddress: "Owerri, Nigeria"), 61 | NewUser(firstname: 'Folake', lastname: 'Adewale', age: 55, homeAddress: "Owerri, Nigeria"), 62 | NewUser(firstname: 'Mustafa', lastname: 'Olawale', age: 56, homeAddress: "Lagos, Nigeria"), 63 | NewUser(firstname: 'Halima', lastname: 'Idris', age: 57, homeAddress: "Lagos, Nigeria"), 64 | NewUser(firstname: 'Chukwuemeka', lastname: 'Okonkwo', age: 58, homeAddress: "Abuja, Nigeria"), 65 | 66 | /// Kenyan Users - [9] 67 | NewUser(firstname: 'Kevin', lastname: 'Luke', age: 37, homeAddress: "Nairobi, Kenya"), 68 | NewUser(firstname: 'Foop', lastname: 'Farr', age: 38, homeAddress: "CBD, Kenya"), 69 | NewUser(firstname: 'Koin', lastname: 'Karl', age: 39, homeAddress: "Mumbasa, Kenya"), 70 | NewUser(firstname: 'Moo', lastname: 'Maa', age: 40, homeAddress: "Westlands, Kenya"), 71 | NewUser(firstname: 'Merh', lastname: 'Merh', age: 41, homeAddress: "Nairobi, Kenya"), 72 | NewUser(firstname: 'Ibrahim', lastname: 'Bakare', age: 42, homeAddress: "Nairobi, Kenya"), 73 | NewUser(firstname: 'Grace', lastname: 'Adegoke', age: 43, homeAddress: "Nairobi, Kenya"), 74 | NewUser(firstname: 'Ahmed', lastname: 'Umar', age: 44, homeAddress: "Nairobi, Kenya"), 75 | NewUser(firstname: 'Nneka', lastname: 'Okoli', age: 45, homeAddress: "Nairobi, Kenya"), 76 | ]; 77 | -------------------------------------------------------------------------------- /packages/yaroorm_test/melos_yaroorm_test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/yaroorm_test/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: yaroorm_test 2 | description: Tests for yaroorm 3 | version: 0.0.1 4 | 5 | publish_to: none 6 | 7 | environment: 8 | sdk: ^3.3.1 9 | 10 | dependencies: 11 | uuid: ^4.4.0 12 | yaroorm: 13 | path: '../' 14 | 15 | dev_dependencies: 16 | test: ^1.25.8 17 | collection: ^1.19.1 18 | build_runner: ^2.4.9 19 | yaroorm_cli: 20 | path: '../yaroorm_cli' -------------------------------------------------------------------------------- /packages/yaroorm_test/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: yaroorm_cli,yaroorm 2 | dependency_overrides: 3 | yaroorm: 4 | path: ../.. 5 | yaroorm_cli: 6 | path: ../yaroorm_cli 7 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/e2e_basic.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:collection/collection.dart'; 3 | import 'package:yaroorm/yaroorm.dart'; 4 | import 'package:yaroorm_test/src/models.dart'; 5 | 6 | import 'package:yaroorm_test/test_data.dart'; 7 | import '../util.dart'; 8 | 9 | void runBasicE2ETest(String connectionName) { 10 | final driver = DB.driver(connectionName); 11 | 12 | return group('with ${driver.type.name} driver', () { 13 | test('driver should connect', () async { 14 | await driver.connect(); 15 | 16 | expect(driver.isOpen, isTrue); 17 | }); 18 | 19 | test( 20 | 'should have no tables', 21 | () async => expect(await driver.hasTable('users'), isFalse), 22 | ); 23 | 24 | test('should execute migration', () async { 25 | await runMigrator(connectionName, 'migrate'); 26 | 27 | expect(await driver.hasTable('users'), isTrue); 28 | }); 29 | 30 | test('should insert single user', () async { 31 | final firstData = usersList.first; 32 | final query = UserQuery.driver(driver); 33 | 34 | final result = await query.insert(NewUser( 35 | firstname: firstData.firstname, 36 | lastname: firstData.lastname, 37 | age: firstData.age, 38 | homeAddress: firstData.homeAddress, 39 | )); 40 | 41 | final exists = await query.where((user) => user.id(result.id)).exists(); 42 | expect(exists, isTrue); 43 | }); 44 | 45 | test('should insert many users', () async { 46 | final query = UserQuery.driver(driver); 47 | 48 | await query.insertMany(usersList.skip(1).toList()); 49 | 50 | expect(await query.count(), usersList.length); 51 | }); 52 | 53 | group('Aggregate Functions', () { 54 | final query = UserQuery.driver(driver).where( 55 | (user) => user.$isLike('home_address', '%%, Ghana'), 56 | ); 57 | List usersInGhana = []; 58 | 59 | setUpAll(() async { 60 | usersInGhana = await query.findMany(); 61 | expect(usersInGhana, isNotEmpty); 62 | }); 63 | 64 | test('sum', () async { 65 | final manualSum = usersInGhana.map((e) => e.age).sum; 66 | expect(await query.sum('age'), equals(manualSum)); 67 | }); 68 | 69 | test('count', () async { 70 | expect(await query.count(), equals(usersInGhana.length)); 71 | }); 72 | 73 | test('max', () async { 74 | final maxAge = usersInGhana.map((e) => e.age).max; 75 | expect(await query.max('age'), equals(maxAge)); 76 | }); 77 | 78 | test('min', () async { 79 | final minAge = usersInGhana.map((e) => e.age).min; 80 | expect(await query.min('age'), equals(minAge)); 81 | }); 82 | 83 | test('average', () async { 84 | final average = usersInGhana.map((e) => e.age).average; 85 | expect(await query.average('age'), equals(average)); 86 | }); 87 | 88 | test('concat', () async { 89 | Matcher matcher(String separator) { 90 | if ([ 91 | DatabaseDriverType.sqlite, 92 | DatabaseDriverType.pgsql, 93 | ].contains(driver.type)) { 94 | return equals(usersInGhana.map((e) => e.age).join(separator)); 95 | } 96 | 97 | return equals( 98 | usersInGhana.map((e) => '${e.age}$separator').join(','), 99 | ); 100 | } 101 | 102 | expect(await query.groupConcat('age', ','), matcher(',')); 103 | }); 104 | }); 105 | 106 | test('should update user', () async { 107 | final query = UserQuery.driver(driver).where((user) => user.id(1)); 108 | 109 | final user = await query.findOne(); 110 | expect(user!.id, 1); 111 | 112 | await query.update(UpdateUser( 113 | firstname: Value('Red Oil'), 114 | age: Value(100), 115 | )); 116 | 117 | final userFromDB = await query.findOne(); 118 | expect(user, isNotNull); 119 | expect(userFromDB?.firstname, 'Red Oil'); 120 | expect(userFromDB?.age, 100); 121 | }); 122 | 123 | test('should update many users', () async { 124 | final userQuery = UserQuery.driver(driver); 125 | final age50Users = userQuery.where((user) => user.age(50)); 126 | 127 | final usersWithAge50 = await age50Users.findMany(); 128 | expect(usersWithAge50.length, 4); 129 | expect(usersWithAge50.every((e) => e.age == 50), isTrue); 130 | 131 | await age50Users.update(UpdateUser(homeAddress: Value('Keta, Ghana'))); 132 | 133 | final updatedResult = await age50Users.findMany(); 134 | expect(updatedResult.length, 4); 135 | expect(updatedResult.every((e) => e.age == 50), isTrue); 136 | expect( 137 | updatedResult.every((e) => e.homeAddress == 'Keta, Ghana'), 138 | isTrue, 139 | ); 140 | }); 141 | 142 | test('should fetch only users in Ghana', () async { 143 | final userQuery = UserQuery.driver(driver).where( 144 | (user) => user.$isLike('home_address', '%, Ghana'), 145 | ); 146 | 147 | final usersInGhana = await userQuery.findMany(); 148 | expect(usersInGhana.length, 10); 149 | expect( 150 | usersInGhana.every((e) => e.homeAddress.contains('Ghana')), 151 | isTrue, 152 | ); 153 | 154 | expect(await userQuery.findMany(limit: 4), hasLength(4)); 155 | }); 156 | 157 | test('should get all users between age 35 and 50', () async { 158 | final age50Users = await UserQuery.driver(driver) 159 | .where((user) => user.$isBetween('age', [35, 50])) 160 | .findMany(orderBy: [OrderUserBy.age(order: OrderDirection.desc)]); 161 | 162 | expect(age50Users.length, 19); 163 | expect(age50Users.first.age, 50); 164 | expect(age50Users.last.age, 35); 165 | }); 166 | 167 | test('should get all users in somewhere in Nigeria', () async { 168 | final users = await UserQuery.driver(driver) 169 | .where((user) => user.$isLike('home_address', '%, Nigeria')) 170 | .findMany(orderBy: [OrderUserBy.homeAddress()]); 171 | 172 | expect(users.length, 18); 173 | expect(users.first.homeAddress, 'Abuja, Nigeria'); 174 | expect(users.last.homeAddress, 'Owerri, Nigeria'); 175 | }); 176 | 177 | test('should get all users where age is 30 or 52', () async { 178 | final users = await UserQuery.driver(driver).where((user) => or([user.age(30), user.age(52)])).findMany(); 179 | 180 | final users2 = await UserQuery.driver(driver).where((user) => user.age(30) | user.age(52)).findMany(); 181 | 182 | expect(users.every((e) => [30, 52].contains(e.age)), isTrue); 183 | expect(users2.every((e) => [30, 52].contains(e.age)), isTrue); 184 | }); 185 | 186 | test('should delete user', () async { 187 | final userQuery = UserQuery.driver(driver); 188 | final userOne = await userQuery.findOne(); 189 | expect(userOne, isNotNull); 190 | 191 | final userOneQuery = userQuery.where((user) => user.id(userOne!.id)); 192 | 193 | await userOneQuery.delete(); 194 | 195 | expect(await userOneQuery.findOne(), isNull); 196 | }); 197 | 198 | test('should delete many users', () async { 199 | final query = UserQuery.driver(driver).where((user) => user.$isLike('home_address', '%, Nigeria')); 200 | expect(await query.findMany(), isNotEmpty); 201 | 202 | await query.delete(); 203 | 204 | expect(await query.findMany(), isEmpty); 205 | }); 206 | 207 | test('should drop tables', () async { 208 | await runMigrator(connectionName, 'migrate:reset'); 209 | 210 | expect(await driver.hasTable('users'), isFalse); 211 | }); 212 | 213 | test('should disconnect', () async { 214 | expect(driver.isOpen, isTrue); 215 | 216 | await driver.disconnect(); 217 | 218 | expect(driver.isOpen, isFalse); 219 | }); 220 | }); 221 | } 222 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/e2e_relation.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:uuid/uuid.dart'; 3 | import 'package:yaroorm/yaroorm.dart'; 4 | import 'package:yaroorm_test/src/models.dart'; 5 | import 'package:yaroorm_test/test_data.dart'; 6 | 7 | import 'package:yaroorm/src/reflection.dart'; 8 | 9 | import '../util.dart'; 10 | 11 | final uuid = Uuid(); 12 | 13 | void runRelationsE2ETest(String connectionName) { 14 | final driver = DB.driver(connectionName); 15 | 16 | return group('with ${driver.type.name} driver', () { 17 | late User testUser1, anotherUser; 18 | 19 | final tableNames = [ 20 | getEntityTableName(), 21 | getEntityTableName(), 22 | getEntityTableName(), 23 | ]; 24 | 25 | setUpAll(() async { 26 | await driver.connect(); 27 | 28 | expect(driver.isOpen, isTrue); 29 | 30 | var hasTables = await Future.wait(tableNames.map(driver.hasTable)); 31 | expect(hasTables.every((e) => e), isFalse); 32 | 33 | await runMigrator(connectionName, 'migrate'); 34 | 35 | hasTables = await Future.wait(tableNames.map(driver.hasTable)); 36 | expect(hasTables.every((e) => e), isTrue); 37 | 38 | testUser1 = await UserQuery.driver(driver).insert(NewUser( 39 | firstname: 'Baba', 40 | lastname: 'Tunde', 41 | age: 29, 42 | homeAddress: 'Owerri, Nigeria', 43 | )); 44 | }); 45 | 46 | test('should add many posts for User', () async { 47 | await testUser1.posts.insertMany([ 48 | NewPostForUser(title: 'Aoo bar 1', description: 'foo bar 4'), 49 | NewPostForUser(title: 'Bee Moo 2', description: 'foo bar 5'), 50 | NewPostForUser(title: 'Coo Kie 3', description: 'foo bar 6'), 51 | ]); 52 | 53 | final posts = await testUser1.posts.get( 54 | orderBy: [OrderPostBy.title(order: OrderDirection.desc)], 55 | ); 56 | expect(posts, hasLength(3)); 57 | expect( 58 | posts.map((e) => {'id': e.id, 'title': e.title, 'desc': e.description, 'userId': e.userId}), 59 | [ 60 | {'id': 3, 'title': 'Coo Kie 3', 'desc': 'foo bar 6', 'userId': 1}, 61 | {'id': 2, 'title': 'Bee Moo 2', 'desc': 'foo bar 5', 'userId': 1}, 62 | {'id': 1, 'title': 'Aoo bar 1', 'desc': 'foo bar 4', 'userId': 1} 63 | ], 64 | ); 65 | }); 66 | 67 | test('should fetch posts with owner', () async { 68 | final posts = await PostQuery.driver(driver).withRelations((post) => [post.owner]).findMany(); 69 | 70 | expect(posts.first.owner.value, isA()); 71 | }); 72 | 73 | test('should add comments for post', () async { 74 | final post = await testUser1.posts.first!; 75 | expect(post, isA()); 76 | 77 | var comments = await post!.comments.get(); 78 | expect(comments, isEmpty); 79 | 80 | final firstId = uuid.v4(); 81 | final secondId = uuid.v4(); 82 | 83 | await post.comments.insertMany([ 84 | NewPostCommentForPost( 85 | id: firstId, 86 | comment: 'A new post looks abit old', 87 | ), 88 | NewPostCommentForPost( 89 | id: secondId, 90 | comment: 'Come, let us explore Dart', 91 | ), 92 | ]); 93 | 94 | comments = await post.comments.get(orderBy: [ 95 | OrderPostCommentBy.comment(order: OrderDirection.desc), 96 | ]); 97 | 98 | expect(comments.every((e) => e.postId == post.id), isTrue); 99 | expect(comments.map((e) => e.id), containsAll([firstId, secondId])); 100 | 101 | expect(comments.map((c) => c.toJson()), [ 102 | {'id': secondId, 'comment': 'Come, let us explore Dart', 'postId': 1}, 103 | {'id': firstId, 'comment': 'A new post looks abit old', 'postId': 1}, 104 | ]); 105 | }); 106 | 107 | test('should add post for another user', () async { 108 | final testuser = usersList.last; 109 | anotherUser = await UserQuery.driver(driver).insert(NewUser( 110 | firstname: testuser.firstname, 111 | lastname: testuser.lastname, 112 | age: testuser.age, 113 | homeAddress: testuser.homeAddress, 114 | )); 115 | 116 | expect(anotherUser.id, isNotNull); 117 | expect(anotherUser.id != testUser1.id, isTrue); 118 | 119 | var anotherUserPosts = await anotherUser.posts.get(); 120 | expect(anotherUserPosts, isEmpty); 121 | 122 | await anotherUser.posts.insert( 123 | NewPostForUser(title: 'Another Post', description: 'wham bamn'), 124 | ); 125 | anotherUserPosts = await anotherUser.posts.get(); 126 | expect(anotherUserPosts, hasLength(1)); 127 | 128 | final anotherUserPost = anotherUserPosts.first; 129 | expect(anotherUserPost.userId, anotherUser.id); 130 | 131 | await anotherUserPost.comments.insertMany([ 132 | NewPostCommentForPost(id: uuid.v4(), comment: 'ah ah'), 133 | NewPostCommentForPost(id: uuid.v4(), comment: 'oh oh'), 134 | ]); 135 | 136 | expect(await anotherUserPost.comments.get(), hasLength(2)); 137 | }); 138 | 139 | test('should delete comments for post', () async { 140 | expect(testUser1, isNotNull); 141 | final posts = await testUser1.posts.get(); 142 | expect(posts, hasLength(3)); 143 | 144 | // ignore: curly_braces_in_flow_control_structures 145 | for (final post in posts) await post.comments.delete(); 146 | 147 | for (final post in posts) { 148 | expect(await post.comments.get(), []); 149 | } 150 | 151 | await testUser1.posts.delete(); 152 | 153 | expect(await testUser1.posts.get(), isEmpty); 154 | }); 155 | 156 | tearDownAll(() async { 157 | await runMigrator(connectionName, 'migrate:reset'); 158 | 159 | final hasTables = await Future.wait(tableNames.map(driver.hasTable)); 160 | expect(hasTables.every((e) => e), isFalse); 161 | 162 | await driver.disconnect(); 163 | expect(driver.isOpen, isFalse); 164 | }); 165 | }); 166 | } 167 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/mariadb.e2e.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import '../../database/database.dart' as db; 4 | import 'e2e_basic.dart'; 5 | import 'e2e_relation.dart'; 6 | 7 | void main() async { 8 | db.initializeORM(); 9 | 10 | group('MariaDB', () { 11 | group('Basic E2E Test', () => runBasicE2ETest('bar_mariadb')); 12 | 13 | group('Relation E2E Test', () => runRelationsE2ETest('bar_mariadb')); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/mysql.e2e.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import '../../database/database.dart' as db; 4 | import 'e2e_basic.dart'; 5 | import 'e2e_relation.dart'; 6 | 7 | void main() async { 8 | db.initializeORM(); 9 | 10 | group('MySQL', () { 11 | group('Basic E2E Test', () => runBasicE2ETest('moo_mysql')); 12 | 13 | group('Relation E2E Test', () => runRelationsE2ETest('moo_mysql')); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/pgsql.e2e.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import '../../database/database.dart' as db; 3 | import 'e2e_basic.dart'; 4 | import 'e2e_relation.dart'; 5 | 6 | void main() async { 7 | db.initializeORM(); 8 | 9 | group('Postgres', () { 10 | group('Basic E2E Test', () => runBasicE2ETest('foo_pgsql')); 11 | 12 | group('Relation E2E Test', () => runRelationsE2ETest('foo_pgsql')); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/integration/sqlite.e2e.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | 3 | import '../../database/database.dart' as db; 4 | import 'e2e_basic.dart'; 5 | import 'e2e_relation.dart'; 6 | 7 | void main() async { 8 | db.initializeORM(); 9 | 10 | group('SQLite', () { 11 | group('Basic E2E Test', () => runBasicE2ETest('foo_sqlite')); 12 | 13 | group('Relation E2E Test', () => runRelationsE2ETest('foo_sqlite')); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | Future runMigrator(String connectionName, String command) async { 6 | final commands = ['run', 'yaroorm_cli', command, '--connection=$connectionName']; 7 | print('> dart ${commands.join(' ')}\n'); 8 | 9 | final result = await Process.run('dart', commands); 10 | stderr.write(result.stderr); 11 | 12 | expect(result.exitCode, 0); 13 | } 14 | -------------------------------------------------------------------------------- /packages/yaroorm_test/test/yaroorm_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:yaroorm/src/database/driver/mysql_driver.dart'; 3 | import 'package:yaroorm/src/database/driver/pgsql_driver.dart'; 4 | import 'package:yaroorm/src/database/driver/sqlite_driver.dart'; 5 | import 'package:yaroorm/yaroorm.dart'; 6 | import 'package:yaroorm_test/src/models.dart'; 7 | 8 | import '../database/database.dart'; 9 | 10 | Matcher throwsArgumentErrorWithMessage(String message) => 11 | throwsA(isA().having((p0) => p0.message, '', message)); 12 | 13 | void main() { 14 | setUpAll(initializeORM); 15 | 16 | group('DatabaseDriver.init', () { 17 | group('when sqlite connection', () { 18 | late DatabaseDriver driver; 19 | 20 | setUpAll(() => driver = DB.driver('foo_sqlite')); 21 | 22 | test('should return SQLite Driver', () { 23 | expect(driver, isA().having((p0) => p0.type, 'has driver type', DatabaseDriverType.sqlite)); 24 | }); 25 | 26 | test('should have table blueprint', () { 27 | expect(driver.blueprint, isA()); 28 | }); 29 | 30 | test('should have primitive serializer', () { 31 | expect(driver.serializer, isA()); 32 | }); 33 | }); 34 | 35 | group('when mysql connection', () { 36 | late DatabaseDriver driver; 37 | 38 | setUpAll(() => driver = DB.driver('moo_mysql')); 39 | 40 | test('should return MySql Driver', () { 41 | expect(driver, isA().having((p0) => p0.type, 'has driver type', DatabaseDriverType.mysql)); 42 | }); 43 | 44 | test('should have table blueprint', () { 45 | expect(driver.blueprint, isA()); 46 | }); 47 | 48 | test('should have primitive serializer', () { 49 | expect(driver.serializer, isA()); 50 | }); 51 | }); 52 | 53 | group('when mariadb connection', () { 54 | late DatabaseDriver driver; 55 | 56 | setUpAll(() => driver = DB.driver('bar_mariadb')); 57 | 58 | test('should return MySql Driver', () { 59 | expect(driver, isA().having((p0) => p0.type, 'has driver type', DatabaseDriverType.mariadb)); 60 | }); 61 | 62 | test('should have table blueprint', () { 63 | expect(driver.blueprint, isA()); 64 | }); 65 | 66 | test('should have primitive serializer', () { 67 | expect(driver.serializer, isA()); 68 | }); 69 | }); 70 | 71 | group('when postgres connection', () { 72 | late DatabaseDriver driver; 73 | 74 | setUpAll(() => driver = DB.driver('foo_pgsql')); 75 | 76 | test('should return Postgres Driver', () { 77 | expect(driver, isA().having((p0) => p0.type, 'has driver type', DatabaseDriverType.pgsql)); 78 | }); 79 | 80 | test('should have table blueprint', () { 81 | expect(driver.blueprint, isA()); 82 | }); 83 | 84 | test('should have primitive serializer', () { 85 | expect(driver.serializer, isA()); 86 | }); 87 | }); 88 | }); 89 | 90 | test('should err when Query without driver', () async { 91 | late Object error; 92 | try { 93 | await Query.table().findMany(); 94 | } catch (e) { 95 | error = e; 96 | } 97 | 98 | expect( 99 | error, 100 | isA() 101 | .having((p0) => p0.message, '', 'Driver not set for query. Make sure you supply a driver using .driver()'), 102 | ); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | ansi_styles: 21 | dependency: transitive 22 | description: 23 | name: ansi_styles 24 | sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "0.3.2+1" 28 | args: 29 | dependency: transitive 30 | description: 31 | name: args 32 | sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.5.0" 36 | async: 37 | dependency: transitive 38 | description: 39 | name: async 40 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.11.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.1" 52 | buffer: 53 | dependency: transitive 54 | description: 55 | name: buffer 56 | sha256: "389da2ec2c16283c8787e0adaede82b1842102f8c8aae2f49003a766c5c6b3d1" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.2.3" 60 | charcode: 61 | dependency: transitive 62 | description: 63 | name: charcode 64 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.3.1" 68 | cli_launcher: 69 | dependency: transitive 70 | description: 71 | name: cli_launcher 72 | sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "0.3.1" 76 | cli_util: 77 | dependency: transitive 78 | description: 79 | name: cli_util 80 | sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "0.4.2" 84 | clock: 85 | dependency: transitive 86 | description: 87 | name: clock 88 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.1.2" 92 | collection: 93 | dependency: "direct main" 94 | description: 95 | name: collection 96 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.18.0" 100 | conventional_commit: 101 | dependency: transitive 102 | description: 103 | name: conventional_commit 104 | sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "0.6.0+1" 108 | convert: 109 | dependency: transitive 110 | description: 111 | name: convert 112 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.1.1" 116 | coverage: 117 | dependency: transitive 118 | description: 119 | name: coverage 120 | sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.7.2" 124 | crypto: 125 | dependency: transitive 126 | description: 127 | name: crypto 128 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "3.0.3" 132 | ffi: 133 | dependency: transitive 134 | description: 135 | name: ffi 136 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.1.2" 140 | file: 141 | dependency: transitive 142 | description: 143 | name: file 144 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "7.0.0" 148 | frontend_server_client: 149 | dependency: transitive 150 | description: 151 | name: frontend_server_client 152 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "4.0.0" 156 | glob: 157 | dependency: transitive 158 | description: 159 | name: glob 160 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "2.1.2" 164 | grammer: 165 | dependency: "direct main" 166 | description: 167 | name: grammer 168 | sha256: "333c0f99fb116ae554276f64769c3a5219d314bb4fc9de43d83ae9746e0b4dd4" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.0.3" 172 | graphs: 173 | dependency: transitive 174 | description: 175 | name: graphs 176 | sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.3.2" 180 | http: 181 | dependency: transitive 182 | description: 183 | name: http 184 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.2.2" 188 | http_multi_server: 189 | dependency: transitive 190 | description: 191 | name: http_multi_server 192 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "3.2.1" 196 | http_parser: 197 | dependency: transitive 198 | description: 199 | name: http_parser 200 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "4.0.2" 204 | intl: 205 | dependency: transitive 206 | description: 207 | name: intl 208 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "0.19.0" 212 | io: 213 | dependency: transitive 214 | description: 215 | name: io 216 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.0.4" 220 | js: 221 | dependency: transitive 222 | description: 223 | name: js 224 | sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "0.7.1" 228 | json_annotation: 229 | dependency: transitive 230 | description: 231 | name: json_annotation 232 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "4.9.0" 236 | lints: 237 | dependency: "direct dev" 238 | description: 239 | name: lints 240 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "3.0.0" 244 | logging: 245 | dependency: transitive 246 | description: 247 | name: logging 248 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.2.0" 252 | matcher: 253 | dependency: transitive 254 | description: 255 | name: matcher 256 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "0.12.16+1" 260 | melos: 261 | dependency: "direct dev" 262 | description: 263 | name: melos 264 | sha256: a62abfa8c7826cec927f8585572bb9adf591be152150494d879ca2c75118809d 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "6.2.0" 268 | meta: 269 | dependency: "direct main" 270 | description: 271 | name: meta 272 | sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.14.0" 276 | mime: 277 | dependency: transitive 278 | description: 279 | name: mime 280 | sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "1.0.5" 284 | mustache_template: 285 | dependency: transitive 286 | description: 287 | name: mustache_template 288 | sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "2.0.0" 292 | mysql_client: 293 | dependency: "direct main" 294 | description: 295 | name: mysql_client 296 | sha256: "6a0fdcbe3e0721c637f97ad24649be2f70dbce2b21ede8f962910e640f753fc2" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "0.0.27" 300 | node_preamble: 301 | dependency: transitive 302 | description: 303 | name: node_preamble 304 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "2.0.2" 308 | package_config: 309 | dependency: transitive 310 | description: 311 | name: package_config 312 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "2.1.0" 316 | path: 317 | dependency: transitive 318 | description: 319 | name: path 320 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.9.0" 324 | pedantic: 325 | dependency: transitive 326 | description: 327 | name: pedantic 328 | sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "1.11.1" 332 | platform: 333 | dependency: transitive 334 | description: 335 | name: platform 336 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "3.1.6" 340 | pool: 341 | dependency: transitive 342 | description: 343 | name: pool 344 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.5.1" 348 | postgres: 349 | dependency: "direct main" 350 | description: 351 | name: postgres 352 | sha256: bc3a36f9960d822af1ac4c2e0a32c4e7a3e426d2ce4500c11afca40f53c34612 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "3.4.0" 356 | process: 357 | dependency: transitive 358 | description: 359 | name: process 360 | sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "5.0.2" 364 | prompts: 365 | dependency: transitive 366 | description: 367 | name: prompts 368 | sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "2.0.0" 372 | pub_semver: 373 | dependency: transitive 374 | description: 375 | name: pub_semver 376 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "2.1.4" 380 | pub_updater: 381 | dependency: transitive 382 | description: 383 | name: pub_updater 384 | sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "0.4.0" 388 | pubspec: 389 | dependency: transitive 390 | description: 391 | name: pubspec 392 | sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "2.3.0" 396 | quiver: 397 | dependency: transitive 398 | description: 399 | name: quiver 400 | sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "3.2.2" 404 | recase: 405 | dependency: "direct main" 406 | description: 407 | name: recase 408 | sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "4.1.0" 412 | sasl_scram: 413 | dependency: transitive 414 | description: 415 | name: sasl_scram 416 | sha256: a47207a436eb650f8fdcf54a2e2587b850dc3caef9973ce01f332b07a6fc9cb9 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "0.1.1" 420 | saslprep: 421 | dependency: transitive 422 | description: 423 | name: saslprep 424 | sha256: "79c9e163a82f55da542feaf0f7a59031e74493299c92008b2b404cd88d639bb4" 425 | url: "https://pub.dev" 426 | source: hosted 427 | version: "1.0.2" 428 | shelf: 429 | dependency: transitive 430 | description: 431 | name: shelf 432 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 433 | url: "https://pub.dev" 434 | source: hosted 435 | version: "1.4.1" 436 | shelf_packages_handler: 437 | dependency: transitive 438 | description: 439 | name: shelf_packages_handler 440 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 441 | url: "https://pub.dev" 442 | source: hosted 443 | version: "3.0.2" 444 | shelf_static: 445 | dependency: transitive 446 | description: 447 | name: shelf_static 448 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 449 | url: "https://pub.dev" 450 | source: hosted 451 | version: "1.1.2" 452 | shelf_web_socket: 453 | dependency: transitive 454 | description: 455 | name: shelf_web_socket 456 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 457 | url: "https://pub.dev" 458 | source: hosted 459 | version: "1.0.4" 460 | source_map_stack_trace: 461 | dependency: transitive 462 | description: 463 | name: source_map_stack_trace 464 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 465 | url: "https://pub.dev" 466 | source: hosted 467 | version: "2.1.1" 468 | source_maps: 469 | dependency: transitive 470 | description: 471 | name: source_maps 472 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 473 | url: "https://pub.dev" 474 | source: hosted 475 | version: "0.10.12" 476 | source_span: 477 | dependency: transitive 478 | description: 479 | name: source_span 480 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 481 | url: "https://pub.dev" 482 | source: hosted 483 | version: "1.10.0" 484 | sqflite_common: 485 | dependency: "direct main" 486 | description: 487 | name: sqflite_common 488 | sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" 489 | url: "https://pub.dev" 490 | source: hosted 491 | version: "2.5.4+4" 492 | sqflite_common_ffi: 493 | dependency: "direct main" 494 | description: 495 | name: sqflite_common_ffi 496 | sha256: a6057d4c87e9260ba1ec436ebac24760a110589b9c0a859e128842eb69a7ef04 497 | url: "https://pub.dev" 498 | source: hosted 499 | version: "2.3.3+1" 500 | sqlite3: 501 | dependency: transitive 502 | description: 503 | name: sqlite3 504 | sha256: "1abbeb84bf2b1a10e5e1138c913123c8aa9d83cd64e5f9a0dd847b3c83063202" 505 | url: "https://pub.dev" 506 | source: hosted 507 | version: "2.4.2" 508 | stack_trace: 509 | dependency: transitive 510 | description: 511 | name: stack_trace 512 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 513 | url: "https://pub.dev" 514 | source: hosted 515 | version: "1.11.1" 516 | stemmer: 517 | dependency: transitive 518 | description: 519 | name: stemmer 520 | sha256: "9a548a410ad690152b7de946c45e8b166f157f2811fb3ad717da3721f5cee144" 521 | url: "https://pub.dev" 522 | source: hosted 523 | version: "2.2.0" 524 | stream_channel: 525 | dependency: transitive 526 | description: 527 | name: stream_channel 528 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 529 | url: "https://pub.dev" 530 | source: hosted 531 | version: "2.1.2" 532 | string_scanner: 533 | dependency: transitive 534 | description: 535 | name: string_scanner 536 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 537 | url: "https://pub.dev" 538 | source: hosted 539 | version: "1.2.0" 540 | synchronized: 541 | dependency: transitive 542 | description: 543 | name: synchronized 544 | sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" 545 | url: "https://pub.dev" 546 | source: hosted 547 | version: "3.1.0+1" 548 | term_glyph: 549 | dependency: transitive 550 | description: 551 | name: term_glyph 552 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 553 | url: "https://pub.dev" 554 | source: hosted 555 | version: "1.2.1" 556 | test: 557 | dependency: "direct dev" 558 | description: 559 | name: test 560 | sha256: d72b538180efcf8413cd2e4e6fcc7ae99c7712e0909eb9223f9da6e6d0ef715f 561 | url: "https://pub.dev" 562 | source: hosted 563 | version: "1.25.4" 564 | test_api: 565 | dependency: transitive 566 | description: 567 | name: test_api 568 | sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" 569 | url: "https://pub.dev" 570 | source: hosted 571 | version: "0.7.1" 572 | test_core: 573 | dependency: transitive 574 | description: 575 | name: test_core 576 | sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" 577 | url: "https://pub.dev" 578 | source: hosted 579 | version: "0.6.2" 580 | tuple: 581 | dependency: transitive 582 | description: 583 | name: tuple 584 | sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 585 | url: "https://pub.dev" 586 | source: hosted 587 | version: "2.0.2" 588 | typed_data: 589 | dependency: transitive 590 | description: 591 | name: typed_data 592 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 593 | url: "https://pub.dev" 594 | source: hosted 595 | version: "1.3.2" 596 | unorm_dart: 597 | dependency: transitive 598 | description: 599 | name: unorm_dart 600 | sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" 601 | url: "https://pub.dev" 602 | source: hosted 603 | version: "0.2.0" 604 | uri: 605 | dependency: transitive 606 | description: 607 | name: uri 608 | sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" 609 | url: "https://pub.dev" 610 | source: hosted 611 | version: "1.0.0" 612 | vm_service: 613 | dependency: transitive 614 | description: 615 | name: vm_service 616 | sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" 617 | url: "https://pub.dev" 618 | source: hosted 619 | version: "14.2.1" 620 | watcher: 621 | dependency: transitive 622 | description: 623 | name: watcher 624 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 625 | url: "https://pub.dev" 626 | source: hosted 627 | version: "1.1.0" 628 | web: 629 | dependency: transitive 630 | description: 631 | name: web 632 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" 633 | url: "https://pub.dev" 634 | source: hosted 635 | version: "0.5.1" 636 | web_socket_channel: 637 | dependency: transitive 638 | description: 639 | name: web_socket_channel 640 | sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" 641 | url: "https://pub.dev" 642 | source: hosted 643 | version: "2.4.5" 644 | webkit_inspection_protocol: 645 | dependency: transitive 646 | description: 647 | name: webkit_inspection_protocol 648 | sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" 649 | url: "https://pub.dev" 650 | source: hosted 651 | version: "1.2.1" 652 | yaml: 653 | dependency: transitive 654 | description: 655 | name: yaml 656 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 657 | url: "https://pub.dev" 658 | source: hosted 659 | version: "3.1.2" 660 | yaml_edit: 661 | dependency: transitive 662 | description: 663 | name: yaml_edit 664 | sha256: e9c1a3543d2da0db3e90270dbb1e4eebc985ee5e3ffe468d83224472b2194a5f 665 | url: "https://pub.dev" 666 | source: hosted 667 | version: "2.2.1" 668 | sdks: 669 | dart: ">=3.5.0 <4.0.0" 670 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: yaroorm 2 | description: Easy migrations, query-builder & ORM for Postgres, SQLite, MySQL & MariaDB. 3 | version: 0.0.4 4 | homepage: https://docs.yaroo.dev/orm/quickstart 5 | repository: https://github.com/codekeyz/yaroo 6 | 7 | topics: 8 | - yaroo 9 | - postgres 10 | - mariadb 11 | - mysql 12 | - sqlite 13 | 14 | environment: 15 | sdk: ^3.3.1 16 | 17 | dependencies: 18 | sqflite_common_ffi: ^2.3.3+1 19 | sqflite_common: ^2.5.4+4 20 | mysql_client: ^0.0.27 21 | postgres: ^3.4.0 22 | 23 | meta: ^1.11.0 24 | grammer: ^1.0.3 25 | recase: ^4.1.0 26 | collection: ^1.18.0 27 | 28 | dev_dependencies: 29 | lints: ^3.0.0 30 | melos: ^6.2.0 31 | test: ^1.24.0 32 | --------------------------------------------------------------------------------