├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── ci.md │ ├── build.md │ ├── chore.md │ ├── documentation.md │ ├── style.md │ ├── test.md │ ├── refactor.md │ ├── performance.md │ ├── revert.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yaml ├── workflows │ ├── main.yaml │ └── wattles.yaml └── PULL_REQUEST_TEMPLATE.md ├── packages └── wattles │ ├── CHANGELOG.md │ ├── analysis_options.yaml │ ├── lib │ ├── src │ │ ├── drivers │ │ │ ├── drivers.dart │ │ │ ├── database_driver.dart │ │ │ └── memory_driver.dart │ │ ├── schemas │ │ │ ├── exceptions │ │ │ │ ├── exceptions.dart │ │ │ │ ├── no_schema_found_error.dart │ │ │ │ └── too_many_schemas_found_error.dart │ │ │ ├── schema_base.dart │ │ │ ├── schemas.dart │ │ │ ├── schema_queryable.dart │ │ │ ├── schema_value.dart │ │ │ ├── schema_property.dart │ │ │ ├── schema_invocation.dart │ │ │ ├── schema_instance.dart │ │ │ └── schema.dart │ │ ├── struct.dart │ │ ├── queries │ │ │ ├── queries.dart │ │ │ ├── operator.dart │ │ │ ├── where.dart │ │ │ ├── query.dart │ │ │ ├── query_builder.dart │ │ │ └── where_builder.dart │ │ ├── data_source.dart │ │ └── data_store.dart │ └── wattles.dart │ ├── .gitignore │ ├── test │ └── src │ │ ├── schemas │ │ ├── exceptions │ │ │ ├── no_schema_found_error_test.dart │ │ │ └── too_many_schemas_error_test.dart │ │ ├── schema_property_test.dart │ │ ├── schema_queryable_test.dart │ │ ├── schema_value_test.dart │ │ ├── schema_invocation_test.dart │ │ ├── schema_test.dart │ │ └── schema_instance_test.dart │ │ ├── data_source_test.dart │ │ ├── queries │ │ ├── where_builder_test.dart │ │ └── query_builder_test.dart │ │ ├── data_store_test.dart │ │ └── drivers │ │ └── memory_driver_test.dart │ ├── example │ ├── schemas │ │ └── todo.dart │ └── main.dart │ ├── pubspec.yaml │ ├── README.md │ ├── LICENSE │ └── coverage_badge.svg ├── .gitignore ├── docs ├── overview.md ├── roadmap.md └── getting-started.md ├── README.md ├── LICENSE └── coverage_badge.svg /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /packages/wattles/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.1-dev.1 2 | 3 | - 🚧 Coming Soon 🚧 4 | -------------------------------------------------------------------------------- /packages/wattles/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.3.0.1.yaml 2 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/drivers/drivers.dart: -------------------------------------------------------------------------------- 1 | export 'database_driver.dart'; 2 | export 'memory_driver.dart'; 3 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/exceptions/exceptions.dart: -------------------------------------------------------------------------------- 1 | export 'no_schema_found_error.dart'; 2 | export 'too_many_schemas_found_error.dart'; 3 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/struct.dart: -------------------------------------------------------------------------------- 1 | /// {@template struct} 2 | /// A class that represents a data structure. 3 | /// {@endtemplate} 4 | abstract class Struct {} 5 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_base.dart: -------------------------------------------------------------------------------- 1 | /// {@template schema_base} 2 | /// The base of any schema. 3 | /// {@endtemplate} 4 | abstract class SchemaBase {} 5 | -------------------------------------------------------------------------------- /packages/wattles/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | pubspec.lock -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/queries.dart: -------------------------------------------------------------------------------- 1 | export 'operator.dart'; 2 | export 'query.dart'; 3 | export 'query_builder.dart'; 4 | export 'where.dart'; 5 | export 'where_builder.dart'; // TODO(wolfen): maybe private it? 6 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/operator.dart: -------------------------------------------------------------------------------- 1 | /// {@template operator} 2 | /// Used for querying data on the database in a typed way. 3 | /// {@endtemplate} 4 | enum Operator { 5 | /// The equals operator. 6 | equals, 7 | } 8 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schemas.dart: -------------------------------------------------------------------------------- 1 | export 'exceptions/exceptions.dart'; 2 | export 'schema.dart'; 3 | export 'schema_instance.dart'; 4 | export 'schema_invocation.dart'; 5 | export 'schema_property.dart'; 6 | export 'schema_queryable.dart'; 7 | export 'schema_value.dart'; 8 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "pub" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | about: Changes to the CI configuration files and scripts 4 | title: "ci: " 5 | labels: ci 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the ci/cd system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The ci system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build System 3 | about: Changes that affect the build system or external dependencies 4 | title: "build: " 5 | labels: build 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the build system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The build system is passing 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/* 18 | 19 | # Visual Studio Code related 20 | .classpath 21 | .project 22 | .settings/ 23 | .vscode/* 24 | 25 | # Coverage 26 | coverage/ -------------------------------------------------------------------------------- /packages/wattles/lib/wattles.dart: -------------------------------------------------------------------------------- 1 | /// Wattle(s) is a strongly typed Dart ORM that does not require code 2 | /// generation. 3 | library wattles; 4 | 5 | export 'src/data_source.dart'; 6 | export 'src/data_store.dart'; 7 | export 'src/drivers/drivers.dart'; 8 | export 'src/queries/queries.dart'; 9 | export 'src/schemas/schemas.dart'; 10 | export 'src/struct.dart'; 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Other changes that don't modify src or test files 4 | title: "chore: " 5 | labels: chore 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | 12 | **Requirements** 13 | 14 | - [ ] No functional changes to the code 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Improve the documentation so all collaborators have a common understanding 4 | title: "docs: " 5 | labels: documentation 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what documentation you are looking to add or improve. 11 | 12 | **Requirements** 13 | 14 | - [ ] Requirements go here 15 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/exceptions/no_schema_found_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template no_schema_found_error} 4 | /// Thrown when no schema is found for a given struct. 5 | /// {@endtemplate} 6 | class NoSchemaFoundError extends Error { 7 | @override 8 | String toString() => 'No schema found for struct $T'; 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Style Changes 3 | about: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 4 | title: "style: " 5 | labels: style 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to change and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | about: Adding missing tests or correcting existing tests 4 | title: "test: " 5 | labels: test 6 | --- 7 | 8 | **Description** 9 | 10 | List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/exceptions/no_schema_found_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | void main() { 5 | group('NoSchemaFoundError', () { 6 | test('toString', () { 7 | expect( 8 | NoSchemaFoundError().toString(), 9 | equals('No schema found for struct Struct'), 10 | ); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/exceptions/too_many_schemas_found_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template too_many_schemas_found_error} 4 | /// Thrown when more than one schema is found for a given struct. 5 | /// {@endtemplate} 6 | class TooManySchemasFoundError extends Error { 7 | @override 8 | String toString() => 'Too many schemas found for struct $T'; 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | concurrency: 4 | group: $-$ 5 | cancel-in-progress: true 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 15 | 16 | pana: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: noop 20 | run: echo 'noop' 21 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/exceptions/too_many_schemas_error_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | void main() { 5 | group('TooManySchemasFoundError', () { 6 | test('toString', () { 7 | expect( 8 | TooManySchemasFoundError().toString(), 9 | equals('Too many schemas found for struct Struct'), 10 | ); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: A code change that neither fixes a bug nor adds a feature 4 | title: "refactor: " 5 | labels: refactor 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Performance Update 3 | about: A code change that improves performance 4 | title: "perf: " 5 | labels: performance 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Revert Commit 3 | about: Reverts a previous commit 4 | title: "revert: " 5 | labels: revert 6 | --- 7 | 8 | **Description** 9 | 10 | Provide a link to a PR/Commit that you are looking to revert and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] Change has been reverted 15 | - [ ] No change in test coverage has happened 16 | - [ ] A new ticket is created for any follow on work that needs to happen 17 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/wattles/example/schemas/todo.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | abstract class Todo extends Struct { 4 | int? id; 5 | 6 | late String title; 7 | 8 | late bool isCompleted; 9 | } 10 | 11 | class TodoSchema extends Schema implements Todo { 12 | TodoSchema() : super(TodoSchema.new, table: 'todos') { 13 | assign(() => id, fromKey: 'id', isPrimary: true); 14 | assign(() => title, fromKey: 'title'); 15 | assign(() => isCompleted, fromKey: 'completed'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/wattles/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: wattles 2 | description: Wattle(s) is a strongly typed Dart ORM that does not require code generation. 3 | version: 0.0.1-dev.1 4 | repository: https://github.com/wolfenrain/wattles 5 | issue_tracker: https://github.com/wolfenrain/wattles/issues 6 | homepage: https://wattl.es 7 | # documentation: https://docs.wattl.es 8 | 9 | environment: 10 | sdk: ">=2.17.0 <3.0.0" 11 | 12 | dependencies: 13 | equatable: ^2.0.3 14 | 15 | dev_dependencies: 16 | mocktail: ^0.3.0 17 | test: ^1.19.2 18 | very_good_analysis: ^3.0.1 19 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 🎯 2 | 3 | Wattle(s) is a strongly typed Dart ORM that does not require code generation. It is inspired by many tools including [TypeORM](https://typeorm.io/), [Mikro ORM](https://mikro-orm.io/), and [Prisma](https://www.prisma.io/). 4 | 5 | The goal of Wattle(s) is to help developers talk to their databases while using a strongly typed API. Wattle(s) main focus is to be a true ORM, create a virtual object database that maps the data to schemas while keeping relations intact. It does not, however, provide a way to create custom queries specifically tailored to your use case or create migrations for your database. 6 | 7 | -------------------------------------------------------------------------------- /.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 | **Steps To Reproduce** 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected Behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional Context** 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/where.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | /// {@template where} 5 | /// A where for filtering in a [Query]. 6 | /// {@endtemplate} 7 | class Where extends Equatable { 8 | /// {@macro where} 9 | const Where(this.property, this.operator, this.value); 10 | 11 | /// The property to filter on. 12 | final SchemaProperty property; 13 | 14 | /// The operation to execute. 15 | final Operator operator; 16 | 17 | /// The value to filter by. 18 | final dynamic value; 19 | 20 | @override 21 | List get props => [property, operator, value]; 22 | } 23 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/query.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | /// {@template query} 5 | /// A query for selecting data from a database. 6 | /// {@endtemplate} 7 | class Query extends Equatable { 8 | /// {@macro query} 9 | const Query(this.wheres, {this.limit}); 10 | 11 | /// The where statements for the query. 12 | /// 13 | /// Each list element is an OR based where statement. 14 | final List> wheres; 15 | 16 | /// The amount of results to limit the query to. 17 | final int? limit; 18 | 19 | @override 20 | List get props => [wheres, limit]; 21 | } 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_queryable.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/src/schemas/schema_base.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | /// {@template schema_queryable} 5 | /// Used in [QueryBuilder] to write queries on a [Struct]. 6 | /// 7 | /// Front-facing it is a [Struct] while mapping all the query statements 8 | /// internally. 9 | /// {@endtemplate} 10 | mixin SchemaQueryable on SchemaBase { 11 | /// Indicates if this [SchemaBase] was created as a queryable or not. 12 | bool isQueryable = false; 13 | 14 | /// Handle the [noSuchMethod] for the queryable schemas. 15 | dynamic noSuchQueryMethod(SchemaInvocation invocation) { 16 | throw (this as Schema) 17 | .getProperty(invocation); // TODO(wolfen): custom exception! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template schema_value} 4 | /// Represents a value in a [SchemaInstance]. 5 | /// 6 | /// It keeps track of the modified state of the value. 7 | /// {@endtemplate} 8 | class SchemaValue { 9 | /// {@macro schema_value} 10 | SchemaValue(this._oldValue) : value = _oldValue; 11 | 12 | dynamic _oldValue; 13 | 14 | /// The current value of the schema value. 15 | dynamic value; 16 | 17 | /// Returns true if this schema value has been modified. 18 | bool get isModified => _oldValue != value; 19 | 20 | /// Persist the new value to the old value. 21 | void persist() => _oldValue = value; 22 | 23 | @override 24 | String toString() => 'SchemaValue(old: $_oldValue, new: $value)'; 25 | } 26 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/drivers/database_driver.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template database_driver} 4 | /// Defines how to interact with a database. 5 | /// {@endtemplate} 6 | abstract class DatabaseDriver { 7 | /// Insert a new row into the database. 8 | Future insert(Schema rootSchema, SchemaInstance instance); 9 | 10 | /// Update an existing row in the database. 11 | Future update( 12 | Schema rootSchema, 13 | SchemaInstance instance, { 14 | required Query query, 15 | }); 16 | 17 | /// Delete a row from the database. 18 | Future delete(Schema rootSchema, {required Query query}); 19 | 20 | /// Query the database for rows. 21 | Future>> query( 22 | Schema rootSchema, { 23 | required Query query, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_property.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// {@template schema_property} 4 | /// Describes a property of a schema. 5 | /// {@endtemplate} 6 | class SchemaProperty extends Equatable implements Exception { 7 | /// {@macro schema_property} 8 | const SchemaProperty( 9 | this.propertyName, { 10 | required this.fromKey, 11 | required this.isPrimary, 12 | required this.isNullable, 13 | }); 14 | 15 | /// The name of the property. 16 | final String propertyName; 17 | 18 | /// The key to use when reading data from a backend. 19 | final String fromKey; 20 | 21 | /// Whether the property is the primary key. 22 | final bool isPrimary; 23 | 24 | /// Whether the property is nullable or not. 25 | final bool isNullable; 26 | 27 | @override 28 | List get props => [propertyName, fromKey, isPrimary, isNullable]; 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Wattle(s) 3 |

4 | 5 |

6 | wattles 7 | coverage 8 | style: very good analysis 9 | License: MIT 10 |

11 | 12 | --- 13 | 14 | > wattles • [ wot-l ] • 15 | > 16 | > a lightweight construction material made by weaving thin branches or slats between upright stakes to form a woven lattice 17 | 18 | Wattle(s) is a strongly typed Dart ORM that does not require code generation. 19 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_property_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | void main() { 6 | group('SchemaProperty', () { 7 | test('can be initialized', () async { 8 | final property = SchemaProperty( 9 | 'propertyName', 10 | fromKey: 'fromKey', 11 | isPrimary: true, 12 | isNullable: false, 13 | ); 14 | 15 | expect(property.propertyName, equals('propertyName')); 16 | expect(property.fromKey, equals('fromKey')); 17 | expect(property.isPrimary, isTrue); 18 | expect(property.isNullable, isFalse); 19 | 20 | expect( 21 | property, 22 | equals( 23 | SchemaProperty( 24 | 'propertyName', 25 | fromKey: 'fromKey', 26 | isPrimary: true, 27 | isNullable: false, 28 | ), 29 | ), 30 | ); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jochum van der Ploeg 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. -------------------------------------------------------------------------------- /packages/wattles/README.md: -------------------------------------------------------------------------------- 1 |

2 | Wattle(s) 3 |

4 | 5 |

6 | Pub 7 | wattles 8 | coverage 9 | style: very good analysis 10 | License: MIT 11 |

12 | 13 | --- 14 | 15 | > wattles • [ wot-l ] • 16 | > 17 | > a lightweight construction material made by weaving thin branches or slats between upright stakes to form a woven lattice 18 | 19 | Wattle(s) is a strongly typed Dart ORM that does not require code generation. 20 | 21 | 🚧 Coming Soon 🚧 22 | -------------------------------------------------------------------------------- /packages/wattles/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jochum van der Ploeg 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. -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/wattles/coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/query_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template query_builder} 4 | /// A builder for creating queries. 5 | /// {@endtemplate} 6 | class QueryBuilder { 7 | /// {@macro query_builder} 8 | QueryBuilder(this._rootSchema, this._executor); 9 | 10 | final Schema _rootSchema; 11 | 12 | final Future> Function(Query) _executor; 13 | 14 | final List> _wheres = []; 15 | 16 | /// Build a where statement for the query. 17 | WhereBuilder where(V Function(T) keyResolver) { 18 | final where = WhereBuilder(_rootSchema.queryable(), keyResolver); 19 | _wheres.add(where); 20 | return where; 21 | } 22 | 23 | /// Execute the query and get all the results. 24 | Future> getMany() async { 25 | final query = Query(_wheres.map((e) => e.build()).toList()); 26 | return _executor(query); 27 | } 28 | 29 | /// Execute the query and get the first result. 30 | Future getOne() async { 31 | final query = Query(_wheres.map((e) => e.build()).toList(), limit: 1); 32 | final results = await _executor(query); 33 | return results.isEmpty ? null : results.first; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/data_source.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:wattles/wattles.dart'; 4 | 5 | /// {@template data_source} 6 | /// The data source is the main entry point for interacting with any database 7 | /// using the Wattle(s) ORM. 8 | /// {@endtemplate} 9 | class DataSource { 10 | /// {@macro data_source} 11 | DataSource.initialize({ 12 | required List schemas, 13 | required this.driver, 14 | }) : _schemas = schemas; 15 | 16 | /// The driver that is used to interact with the database. 17 | final DatabaseDriver driver; 18 | 19 | /// The schemas that are available in this data source. 20 | List get schemas => UnmodifiableListView(_schemas); 21 | final List _schemas; 22 | 23 | /// Returns a [DataStore] instance for the given [Struct]. 24 | DataStore getStore() { 25 | final schemas = _schemas.whereType(); 26 | if (schemas.isEmpty) { 27 | throw NoSchemaFoundError(); 28 | } 29 | if (schemas.length > 1) { 30 | throw TooManySchemasFoundError(); 31 | } 32 | final rootSchema = schemas.first; 33 | 34 | return DataStore( 35 | schema: rootSchema as Schema, 36 | source: this, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/wattles.yaml: -------------------------------------------------------------------------------- 1 | name: wattles 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/workflows/wattles.yaml 9 | - packages/wattles/** 10 | 11 | pull_request: 12 | branches: 13 | - main 14 | paths: 15 | - .github/workflows/wattles.yaml 16 | - packages/wattles/** 17 | 18 | jobs: 19 | 20 | build: 21 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 22 | with: 23 | working_directory: packages/wattles 24 | 25 | pana: 26 | defaults: 27 | run: 28 | working-directory: packages/wattles 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v2.3.4 34 | - uses: dart-lang/setup-dart@v1 35 | 36 | - name: Install Dependencies 37 | run: | 38 | dart pub get 39 | dart pub global activate pana 40 | 41 | - name: Verify Pub Score 42 | run: | 43 | PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p") 44 | echo "score: $PANA_SCORE" 45 | IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1] 46 | if (( $SCORE < $TOTAL )); then echo "minimum score not met!"; exit 1; fi -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_queryable_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | late int testProperty1; 7 | } 8 | 9 | class _TestSchema extends Schema implements _TestStruct { 10 | _TestSchema() : super(_TestSchema.new, table: 'test') { 11 | assign(() => testProperty1, fromKey: 'test_property_1'); 12 | } 13 | } 14 | 15 | void main() { 16 | group('SchemaQueryable', () { 17 | late _TestSchema schema; 18 | late SchemaQueryable queryable; 19 | 20 | setUp(() { 21 | schema = _TestSchema(); 22 | queryable = schema.queryable(); 23 | }); 24 | 25 | test('is a queryable', () async { 26 | expect(queryable.isQueryable, isTrue); 27 | }); 28 | 29 | test('throws as SchemaProperty when trying to access a property', () async { 30 | expect( 31 | () => (queryable as _TestStruct).testProperty1, 32 | throwsA( 33 | equals( 34 | SchemaProperty( 35 | 'testProperty1', 36 | fromKey: 'test_property_1', 37 | isPrimary: false, 38 | isNullable: false, 39 | ), 40 | ), 41 | ), 42 | ); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_value_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | void main() { 6 | group('SchemaValue', () { 7 | test('returns current value', () { 8 | final value = SchemaValue(1); 9 | expect(value.value, equals(1)); 10 | 11 | value.value = 2; 12 | expect(value.value, equals(2)); 13 | }); 14 | 15 | group('isModified', () { 16 | test('becomes modified when value changes', () { 17 | final value = SchemaValue(1)..value = 2; 18 | 19 | expect(value.isModified, isTrue); 20 | }); 21 | 22 | test('becomes unmodified when value is set to the same value', () { 23 | final value = SchemaValue(1)..value = 1; 24 | 25 | expect(value.isModified, isFalse); 26 | }); 27 | }); 28 | 29 | group('persist', () { 30 | test('persists value', () { 31 | final value = SchemaValue(1) 32 | ..value = 2 33 | ..persist(); 34 | 35 | expect(value.value, equals(2)); 36 | expect(value.isModified, isFalse); 37 | }); 38 | }); 39 | 40 | test('toString', () { 41 | final value = SchemaValue(1)..value = 2; 42 | 43 | expect(value.toString(), equals('SchemaValue(old: 1, new: 2)')); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/wattles/example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | import 'schemas/todo.dart'; 4 | 5 | void main() async { 6 | final dataSource = DataSource.initialize( 7 | schemas: [ 8 | TodoSchema(), 9 | ], 10 | driver: MemoryDriver(), 11 | ); 12 | 13 | final todoStore = dataSource.getStore(); 14 | 15 | /// Create a fresh todo in memory. 16 | final firstTodo = todoStore.create() 17 | ..title = 'Buy milk' 18 | ..isCompleted = false; 19 | 20 | /// Save the todo to the database. 21 | await todoStore.save(firstTodo); 22 | 23 | /// Change the todo and save it again. 24 | firstTodo.isCompleted = true; 25 | await todoStore.save(firstTodo); 26 | 27 | /// Create a query for finding todos. 28 | final queryBuilder = todoStore.query() 29 | ..where((todo) => todo.title) 30 | .equals('Buy milk') 31 | .and((todo) => todo.isCompleted) 32 | .equals(true) 33 | ..where((todo) => todo.title) 34 | .equals('Buy bread') 35 | .and((todo) => todo.isCompleted) 36 | .equals(true); 37 | 38 | /// Get all todos that match the query. 39 | final foundTodos = await queryBuilder.getMany(); 40 | 41 | /// Loop over the found todos and change them, and save them again. 42 | for (final todo in foundTodos) { 43 | todo.isCompleted = false; 44 | await todoStore.save(todo); 45 | } 46 | 47 | /// Delete the first todo. 48 | await todoStore.delete(firstTodo); 49 | } 50 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/queries/where_builder.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_returning_this 2 | 3 | import 'package:wattles/wattles.dart'; 4 | 5 | /// {@template where_builder} 6 | /// A builder for creating where statements. 7 | /// {@endtemplate} 8 | class WhereBuilder { 9 | /// {@macro where_builder} 10 | WhereBuilder(this._queryable, this._keyResolver); 11 | 12 | final List> _ands = []; 13 | 14 | Where? _result; 15 | 16 | final SchemaQueryable _queryable; 17 | 18 | final V Function(T) _keyResolver; 19 | 20 | /// Turn the where into a equal operator where statement. 21 | WhereBuilder equals(V value) { 22 | if (_result != null) { 23 | // TODO(wolfen): better error? 24 | throw Exception('Where already built.'); 25 | } 26 | try { 27 | _keyResolver(_queryable as T); 28 | } on SchemaProperty catch (e) { 29 | _result = Where(e, Operator.equals, value); 30 | } 31 | return this; 32 | } 33 | 34 | /// Add another clause to the where statement. 35 | WhereBuilder and(C Function(T) whereStatement) { 36 | final where = WhereBuilder(_queryable, whereStatement); 37 | _ands.add(where); 38 | return where; 39 | } 40 | 41 | /// Build the where statement. 42 | List build() { 43 | if (_result == null) { 44 | // TODO(wolfen): better error? 45 | throw Exception('Where not built.'); 46 | } 47 | return [_result!, for (final and in _ands) ...and.build()]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 🗺️ 2 | 3 | As this package is highly experimental and new, a high-level overview of the roadmap can be useful. It will allow people to more easily track what features Wattle(s) is getting and what their states are. 4 | 5 | ## Areas of Focus 💡 6 | 7 | ### Production Readiness ⚙️ 8 | 9 | #### Testing 🧪 10 | 11 | - [ ] 100% test coverage for all packages 12 | 13 | #### Documentation 🗒️ 14 | 15 | - [ ] Comprehensive Documentation for getting started with Wattle(s) 16 | - [x] Creating a `Struct` 17 | - [x] Creating a `Schema` 18 | - [ ] Create a `DataSource` 19 | - [ ] Showcase by getting a `DataStore` for the struct 20 | - [ ] Document how to write a driver implementation 21 | - [ ] Documentation Site 22 | 23 | ### Features ✨ 24 | 25 | - [x] Map a schema to a table. 26 | - [ ] Validating data, both incoming and outgoing 27 | - [ ] Support relations 28 | - [ ] One to one 29 | - [ ] Many to one 30 | - [ ] One to many 31 | - [ ] Many to Many 32 | - [ ] Make the query builder more versatile yet stricter 33 | - `WhereBuilder` operator methods should return a `WhereResult` that only has an `and` method 34 | - Support the at least the following WHERE operators 35 | - [ ] Equals and not equals (`=`, `!=`) 36 | - [ ] Greater than and it's equals variant (`>`, `>=`) 37 | - [ ] Less than and it's equal variant (`<`, `<=`) 38 | - [ ] Add a way to stream query results: `streamQuery` 39 | - [x] Support in-memory driver 40 | - [ ] Support SQL based driver 41 | - [ ] Support Redis driver -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_invocation.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | /// {@template schema_invocation} 4 | /// 5 | /// {@endtemplate} 6 | class SchemaInvocation extends Equatable implements Exception { 7 | /// {@macro schema_invocation} 8 | /// 9 | /// Tests the given [func] and if it throws an [SchemaInvocation] it returns 10 | /// the [SchemaInvocation] otherwise it throws an [UnimplementedError] 11 | /// stating that the given [func] has not implemented a [SchemaInvocation]. 12 | factory SchemaInvocation( 13 | Function func, { 14 | List arguments = const [], 15 | }) { 16 | try { 17 | Function.apply(func, arguments); 18 | } on SchemaInvocation catch (err) { 19 | return err; 20 | } 21 | throw UnimplementedError( 22 | 'SchemaInvocation not implemented on given function: $func', 23 | ); 24 | } 25 | 26 | /// {@macro schema_invocation} 27 | /// 28 | /// Construct one from a given [Invocation]. 29 | const SchemaInvocation.fromInvocation(this.invocation); 30 | 31 | /// The original invocation. 32 | final Invocation invocation; 33 | 34 | /// The member name of the invocation. 35 | String get memberName => invocation.memberName.name; 36 | 37 | /// Whether the invocation is setting a value. 38 | bool get isSetter => invocation.isSetter; 39 | 40 | /// The positional arguments from the [invocation]. 41 | List get positionalArguments => invocation.positionalArguments; 42 | 43 | @override 44 | List get props => [memberName, isSetter, positionalArguments]; 45 | } 46 | 47 | final _symbolRegexp = RegExp(r'Symbol\("([a-zA-Z0-9_]+)([=]?)"\)'); 48 | 49 | extension on Symbol { 50 | Match? get match => _symbolRegexp.firstMatch('$this'); 51 | 52 | String get name => match!.group(1)!; 53 | } 54 | -------------------------------------------------------------------------------- /packages/wattles/test/src/data_source_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | int? id; 7 | 8 | late String key; 9 | } 10 | 11 | class _TestSchema extends Schema implements _TestStruct { 12 | _TestSchema() : super(_TestSchema.new, table: 'test') { 13 | assign(() => id, fromKey: 'id', isPrimary: true); 14 | assign(() => key, fromKey: 'key'); 15 | } 16 | } 17 | 18 | void main() { 19 | group('DataSource', () { 20 | test('initialize correctly', () { 21 | final dataSource = DataSource.initialize( 22 | schemas: [_TestSchema()], 23 | driver: MemoryDriver(), 24 | ); 25 | 26 | expect(dataSource.schemas.length, equals(1)); 27 | expect(dataSource.schemas.first, isA<_TestSchema>()); 28 | }); 29 | 30 | group('getStore', () { 31 | test('gets a DataStore instance for given schema', () { 32 | final dataSource = DataSource.initialize( 33 | schemas: [_TestSchema()], 34 | driver: MemoryDriver(), 35 | ); 36 | 37 | final store = dataSource.getStore<_TestStruct>(); 38 | 39 | expect(store, isA>()); 40 | }); 41 | 42 | test('throws an exception if schema is not found', () { 43 | final dataSource = DataSource.initialize( 44 | schemas: [], 45 | driver: MemoryDriver(), 46 | ); 47 | 48 | expect( 49 | () => dataSource.getStore<_TestStruct>(), 50 | throwsA(isA()), 51 | ); 52 | }); 53 | 54 | test('throws an exception if schema is not found', () { 55 | final dataSource = DataSource.initialize( 56 | schemas: [_TestSchema(), _TestSchema()], 57 | driver: MemoryDriver(), 58 | ); 59 | 60 | expect( 61 | () => dataSource.getStore<_TestStruct>(), 62 | throwsA(isA()), 63 | ); 64 | }); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema_instance.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/src/schemas/schema_base.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | /// {@template schema_instance} 5 | /// An instance of a schema that holds data. 6 | /// 7 | /// Front-facing it is a [Struct] while mapping all properties to the [_data]. 8 | /// {@endtemplate} 9 | mixin SchemaInstance on SchemaBase { 10 | /// Indicates if this [SchemaBase] was created as an instance or not. 11 | bool isInstance = false; 12 | 13 | final Map _data = {}; 14 | 15 | /// Get the value of a property. 16 | SchemaValue? get(SchemaProperty property) { 17 | final value = _data[property.fromKey]; 18 | if (!property.isNullable && (value == null || value.value == null)) { 19 | // TODO(wolfen): better errors 20 | throw Exception( 21 | ''' 22 | Property ${property.propertyName} is non nullable but is currently null. 23 | 24 | Did you forget to assign a value to it?''', 25 | ); 26 | } 27 | return _data[property.fromKey]; 28 | } 29 | 30 | /// Set the value of a property. 31 | /// 32 | /// This method also validates if the value is valid for the property. 33 | void set(SchemaProperty property, dynamic value) { 34 | if (!property.isNullable && value == null) { 35 | // TODO(wolfen): proper error handling. 36 | throw Exception('Property ${property.propertyName} is not nullable'); 37 | } 38 | if (!_data.containsKey(property.fromKey)) { 39 | _data[property.fromKey] = SchemaValue(value); 40 | } else { 41 | _data[property.fromKey]!.value = value; 42 | } 43 | } 44 | 45 | /// Handle the [noSuchMethod] for the instance schemas. 46 | dynamic noSuchInstanceMethod(SchemaInvocation invocation) { 47 | final property = (this as Schema).getProperty(invocation); 48 | if (invocation.isSetter) { 49 | set(property, invocation.positionalArguments[0]); 50 | } 51 | return get(property)?.value; 52 | } 53 | 54 | /// Create a string representation of the instance. 55 | String toInstanceString() { 56 | return [ 57 | '{', 58 | for (final prop in (this as Schema).properties) ...[ 59 | '${prop.propertyName}: ${_data[prop.fromKey]?.value.toString()}', 60 | if (prop != (this as Schema).properties.last) ', ', 61 | ], 62 | '}' 63 | ].join(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 🚀 2 | 3 | ## Prerequisites 📝 4 | 5 | In order to use Wattle(s) you must have the [Dart SDK][dart_installation_link] installed on your machine. 6 | 7 | > **Note**: Wattle(s) requires Dart `">=2.17.0 <3.0.0"` 8 | 9 | ## Installing 🧑‍💻 10 | 11 | Let's start by installing the [`wattles`](https://pub.dev/packages/wattles) package, this package comes with all the core functionality to get started. 12 | 13 | ```shell 14 | # 📦 Install the wattles package from pub.dev 15 | dart pub add wattles 16 | ``` 17 | 18 | ## Defining `Struct`s and `Schema`s 🏗️ 19 | 20 | `Struct`s are the strongly typed representation of how we want to interact with our data. They are used as an interface and therefore should never be initialized directly. 21 | 22 | Lets define a Todo `Struct`: 23 | 24 | ```dart 25 | // By extending from Struct we tell Wattle(s) that this class is our strongly typed interface. 26 | abstract class Todo extends Struct { 27 | // Because id is nullable, we don't have to define it as late. 28 | int? id; 29 | 30 | // By defining this as a non-nullable String we ensure that this value won't 31 | // ever be null. Wattle(s) can use this to validate incoming data from the 32 | // database! 33 | late String title; 34 | 35 | late bool isCompleted; 36 | } 37 | ``` 38 | 39 | > **Note**: We can name properties on a `Struct` that don't directly have to match the same naming convention on the data source, this allows us to have the front facing code more in-line with what we would want in our application. 40 | 41 | Now that we have our `Struct` defined we can create a `Schema` to map the `Todo` to the database: 42 | 43 | ```dart 44 | // We extends Schema and implement our Todo struct so that Wattle(s) knows that 45 | // this is the schema for our Todo and by doing so everything will be strongly 46 | // typed. 47 | class TodoSchema extends Schema implements Todo { 48 | // The super call receives the schema's constructor and what table will 49 | // hold the data we need. 50 | TodoSchema() : super(TodoSchema.new, table: 'todos') { 51 | // We map each of the properties to their data representation, and we can 52 | // even define extra meta data if required. 53 | assign(() => id, fromKey: 'id', isPrimary: true); 54 | assign(() => title, fromKey: 'title'); 55 | assign(() => isCompleted, fromKey: 'completed'); 56 | } 57 | } 58 | ``` 59 | 60 | [dart_installation_link]: https://dart.dev/get-dart -------------------------------------------------------------------------------- /packages/wattles/test/src/queries/where_builder_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | late int testProperty1; 7 | late int testProperty2; 8 | } 9 | 10 | class _TestSchema extends Schema implements _TestStruct { 11 | _TestSchema() : super(_TestSchema.new, table: 'test') { 12 | assign(() => testProperty1, fromKey: 'test_property_1'); 13 | assign(() => testProperty2, fromKey: 'test_property_2'); 14 | } 15 | } 16 | 17 | void main() { 18 | group('WhereBuilder', () { 19 | late _TestSchema schema; 20 | 21 | setUp(() { 22 | schema = _TestSchema(); 23 | }); 24 | 25 | group('equals', () { 26 | test('where statement can be created', () { 27 | final whereBuilder = WhereBuilder<_TestStruct, int>( 28 | schema.queryable(), 29 | (test) => test.testProperty1, 30 | ).equals(1); 31 | 32 | expect( 33 | whereBuilder.build(), 34 | equals([Where(schema.properties.first, Operator.equals, 1)]), 35 | ); 36 | }); 37 | 38 | test('fails if a previous operator was used', () { 39 | final whereBuilder = WhereBuilder<_TestStruct, int>( 40 | schema.queryable(), 41 | (test) => test.testProperty1, 42 | ).equals(1); 43 | 44 | expectLater(() => whereBuilder.equals(1), throwsException); 45 | }); 46 | }); 47 | 48 | test('can chain multiple where statements', () { 49 | final whereBuilder = WhereBuilder<_TestStruct, int>( 50 | schema.queryable(), 51 | (test) => test.testProperty1, 52 | ).equals(1); 53 | 54 | final secondBuilder = 55 | whereBuilder.and((test) => test.testProperty2).equals(3); 56 | 57 | expect( 58 | whereBuilder.build(), 59 | equals([ 60 | Where(schema.properties.first, Operator.equals, 1), 61 | Where(schema.properties.last, Operator.equals, 3), 62 | ]), 63 | ); 64 | 65 | expect(whereBuilder, isNot(equals(secondBuilder))); 66 | }); 67 | 68 | test('fails to build if no operator was used', () { 69 | final whereBuilder = WhereBuilder<_TestStruct, int>( 70 | schema.queryable(), 71 | (test) => test.testProperty1, 72 | ); 73 | 74 | expectLater(whereBuilder.build, throwsException); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_invocation_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:mocktail/mocktail.dart'; 3 | import 'package:test/test.dart'; 4 | import 'package:wattles/wattles.dart'; 5 | 6 | abstract class _TestStruct extends Struct { 7 | late int testProperty1; 8 | } 9 | 10 | class _TestSchema extends Schema implements _TestStruct { 11 | _TestSchema() : super(_TestSchema.new, table: 'test') { 12 | assign(() => testProperty1, fromKey: 'test_property_1'); 13 | } 14 | } 15 | 16 | class _MockInvocation extends Mock implements Invocation {} 17 | 18 | void main() { 19 | group('SchemaInvocation', () { 20 | late _TestSchema schema; 21 | late Invocation invocation; 22 | 23 | setUp(() { 24 | schema = _TestSchema(); 25 | invocation = _MockInvocation(); 26 | }); 27 | 28 | test('construct a SchemaInvocation from a property', () async { 29 | final schemaInvocation = SchemaInvocation(() => schema.testProperty1); 30 | 31 | when(() => invocation.memberName).thenReturn(#testProperty1); 32 | when(() => invocation.isSetter).thenReturn(false); 33 | when(() => invocation.positionalArguments).thenReturn([]); 34 | 35 | expect( 36 | schemaInvocation, 37 | equals(SchemaInvocation.fromInvocation(invocation)), 38 | ); 39 | 40 | verify(() => invocation.memberName).called(1); 41 | verify(() => invocation.isSetter).called(1); 42 | verify(() => invocation.positionalArguments).called(1); 43 | }); 44 | 45 | test('fails to construct a SchemaInvocation', () async { 46 | expect( 47 | () => SchemaInvocation(() => 'testing'), 48 | throwsUnimplementedError, 49 | ); 50 | }); 51 | 52 | test('construct a SchemaInvocation from a Invocation ', () { 53 | when(() => invocation.memberName).thenReturn(#testProperty1); 54 | when(() => invocation.isSetter).thenReturn(false); 55 | when(() => invocation.positionalArguments).thenReturn([]); 56 | 57 | final schemaInvocation = SchemaInvocation.fromInvocation(invocation); 58 | 59 | expect(schemaInvocation.memberName, equals('testProperty1')); 60 | expect(schemaInvocation.isSetter, isFalse); 61 | expect(schemaInvocation.positionalArguments, isEmpty); 62 | 63 | verify(() => invocation.memberName).called(1); 64 | verify(() => invocation.isSetter).called(1); 65 | verify(() => invocation.positionalArguments).called(1); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | int? testProperty1; 7 | } 8 | 9 | class _TestSchema extends Schema implements _TestStruct { 10 | _TestSchema() : super(_TestSchema.new, table: 'test'); 11 | } 12 | 13 | void main() { 14 | group('Schema', () { 15 | late _TestSchema schema; 16 | 17 | setUp(() { 18 | schema = _TestSchema(); 19 | }); 20 | 21 | test('has the correct table', () { 22 | expect(schema.table, equals('test')); 23 | }); 24 | 25 | test('creates an instance', () async { 26 | expect(schema.instance().isInstance, isTrue); 27 | }); 28 | 29 | test('creates an queryable', () async { 30 | expect(schema.queryable().isQueryable, isTrue); 31 | }); 32 | 33 | test('assign a property by invocation', () { 34 | schema.assign(() => schema.testProperty1, fromKey: 'test_property_1'); 35 | 36 | expect( 37 | schema.properties, 38 | equals([ 39 | SchemaProperty( 40 | 'testProperty1', 41 | fromKey: 'test_property_1', 42 | isPrimary: false, 43 | isNullable: true, 44 | ), 45 | ]), 46 | ); 47 | }); 48 | 49 | test('get property by a schema invocation', () { 50 | schema.assign(() => schema.testProperty1, fromKey: 'test_property_1'); 51 | 52 | expect( 53 | schema.getProperty(SchemaInvocation(() => schema.testProperty1)), 54 | equals(schema.properties.first), 55 | ); 56 | }); 57 | 58 | group('toString', () { 59 | test('returns a string representation of the schema', () { 60 | expect(schema.toString(), equals("Instance of '_TestSchema'")); 61 | }); 62 | 63 | test('calls instance toString', () { 64 | final instance = schema.instance(); 65 | expect(instance.toString(), equals('{}')); 66 | }); 67 | }); 68 | 69 | test('set data to a schema instance', () { 70 | schema.assign(() => schema.testProperty1, fromKey: 'test_property_1'); 71 | final instance = schema.instance(); 72 | 73 | // Needed as the _TestSchema does not have a assign of it's own. 74 | (instance as _TestSchema) 75 | .assign(() => schema.testProperty1, fromKey: 'test_property_1'); 76 | 77 | Schema.setAll(schema, instance, { 78 | 'test_property_1': 1, 79 | }); 80 | 81 | expect(instance.testProperty1, equals(1)); 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /packages/wattles/test/src/schemas/schema_instance_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | late int testProperty1; 7 | 8 | int? testProperty2; 9 | } 10 | 11 | class _TestSchema extends Schema implements _TestStruct { 12 | _TestSchema() : super(_TestSchema.new, table: 'test') { 13 | assign(() => testProperty1, fromKey: 'test_property_1'); 14 | assign(() => testProperty2, fromKey: 'test_property_2'); 15 | } 16 | } 17 | 18 | void main() { 19 | group('SchemaInstance', () { 20 | late _TestSchema schema; 21 | late SchemaInstance instance; 22 | 23 | setUp(() { 24 | schema = _TestSchema(); 25 | instance = schema.instance(); 26 | }); 27 | 28 | test('is an instance', () async { 29 | expect(instance.isInstance, isTrue); 30 | }); 31 | 32 | group('value mapping', () { 33 | test('setting a value based on a property', () { 34 | (instance as _TestStruct).testProperty1 = 1; 35 | 36 | expect(instance.get(schema.properties.first)?.value, equals(1)); 37 | }); 38 | 39 | test('getting a value based on a property', () { 40 | instance.set(schema.properties.first, 1); 41 | 42 | expect((instance as _TestStruct).testProperty1, equals(1)); 43 | }); 44 | 45 | test('overwriting a value based on a property', () { 46 | (instance as _TestStruct).testProperty1 = 1; 47 | 48 | expect(instance.get(schema.properties.first)?.isModified, isFalse); 49 | (instance as _TestStruct).testProperty1 = 2; 50 | 51 | expect((instance as _TestStruct).testProperty1, equals(2)); 52 | expect(instance.get(schema.properties.first)?.isModified, isTrue); 53 | }); 54 | 55 | test('throws an error if validation of the setting value fails', () { 56 | expect( 57 | () => instance.set(schema.properties.first, null), 58 | throwsException, 59 | ); 60 | }); 61 | 62 | test('throws an error if validation of the getting value fails', () { 63 | expect( 64 | () => instance.get(schema.properties.first), 65 | throwsException, 66 | ); 67 | }); 68 | }); 69 | 70 | group('toString', () { 71 | test('returns a string representation of the instance', () { 72 | instance.set(schema.properties.first, 1); 73 | 74 | expect( 75 | instance.toString(), 76 | equals('{testProperty1: 1, testProperty2: null}'), 77 | ); 78 | }); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /packages/wattles/test/src/queries/query_builder_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | late int testProperty1; 7 | } 8 | 9 | class _TestSchema extends Schema implements _TestStruct { 10 | _TestSchema() : super(_TestSchema.new, table: 'test') { 11 | assign(() => testProperty1, fromKey: 'test_property_1'); 12 | } 13 | } 14 | 15 | void main() { 16 | group('QueryBuilder', () { 17 | late _TestSchema schema; 18 | late _TestStruct instance; 19 | 20 | setUp(() { 21 | schema = _TestSchema(); 22 | instance = (_TestSchema().instance() as _TestStruct)..testProperty1 = 1; 23 | }); 24 | 25 | group('getMany', () { 26 | test('build and execute an empty query', () async { 27 | final queryBuilder = QueryBuilder<_TestStruct>(schema, (query) async { 28 | expect(query, equals(Query(const []))); 29 | return [instance]; 30 | }); 31 | 32 | final result = await queryBuilder.getMany(); 33 | expect(result, isA>()); 34 | expect(result.length, equals(1)); 35 | }); 36 | 37 | test('build and execute a query with a where statement', () async { 38 | final queryBuilder = QueryBuilder<_TestStruct>(schema, (query) async { 39 | expect( 40 | query, 41 | equals( 42 | Query([ 43 | [Where(schema.properties.first, Operator.equals, 1)] 44 | ]), 45 | ), 46 | ); 47 | return [instance]; 48 | }) 49 | ..where((test) => test.testProperty1).equals(1); 50 | 51 | final result = await queryBuilder.getMany(); 52 | expect(result, isA>()); 53 | expect(result.length, equals(1)); 54 | }); 55 | }); 56 | 57 | group('getOne', () { 58 | test('build and execute an empty query', () async { 59 | final queryBuilder = QueryBuilder<_TestStruct>(schema, (query) async { 60 | expect(query, equals(Query(const [], limit: 1))); 61 | return [instance]; 62 | }); 63 | 64 | final result = await queryBuilder.getOne(); 65 | expect(result, isA<_TestStruct>()); 66 | }); 67 | 68 | test('build and execute a query with a where statement', () async { 69 | final queryBuilder = QueryBuilder<_TestStruct>(schema, (query) async { 70 | expect( 71 | query, 72 | equals( 73 | Query( 74 | [ 75 | [Where(schema.properties.first, Operator.equals, 1)] 76 | ], 77 | limit: 1, 78 | ), 79 | ), 80 | ); 81 | return [instance]; 82 | }) 83 | ..where((test) => test.testProperty1).equals(1); 84 | 85 | final result = await queryBuilder.getOne(); 86 | expect(result, isA<_TestStruct>()); 87 | }); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/schemas/schema.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/src/schemas/schema_base.dart'; 2 | import 'package:wattles/wattles.dart'; 3 | 4 | /// {@template schema} 5 | /// A schema represents the properties of a [Struct]. It is used to validate 6 | /// the properties of a [SchemaInstance] when it is created or updated. 7 | /// 8 | /// Every class that extends this will be considered the root schema of a 9 | /// [Struct]. 10 | /// 11 | /// It extends [SchemaBase] purely for the purpose of being able to cast to 12 | /// either [SchemaInstance] or [SchemaQueryable]. 13 | abstract class Schema extends SchemaBase with SchemaInstance, SchemaQueryable { 14 | /// {@macro schema} 15 | Schema(this._create, {required this.table}); 16 | 17 | /// Create an instance of the [Schema]. 18 | final Schema Function() _create; 19 | 20 | /// The name of the table for this schema in the database. 21 | final String table; 22 | 23 | /// Create a new instance of the [Schema]. Used for storing data locally. 24 | SchemaInstance instance() => _create()..isInstance = true; 25 | 26 | /// Create a new instance of the [Schema]. Used for querying data. 27 | SchemaQueryable queryable() => _create()..isQueryable = true; 28 | 29 | /// The properties of the [Schema] that are mapped through [assign]. 30 | final List properties = []; 31 | 32 | /// Assign a given [Struct] property and map its attributes to what it is in 33 | /// the database. 34 | void assign( 35 | T Function() key, { 36 | required String fromKey, 37 | bool isPrimary = false, 38 | }) { 39 | final invocation = SchemaInvocation(key); 40 | 41 | properties.add( 42 | SchemaProperty( 43 | invocation.memberName, 44 | fromKey: fromKey, 45 | isPrimary: isPrimary, 46 | isNullable: '$T'.endsWith('?'), 47 | ), 48 | ); 49 | } 50 | 51 | /// Get the [SchemaProperty] for a given [Struct] property. 52 | SchemaProperty getProperty(SchemaInvocation invocation) { 53 | return properties.firstWhere( 54 | (property) => property.propertyName == invocation.memberName, 55 | ); 56 | } 57 | 58 | @override 59 | dynamic noSuchMethod(Invocation invocation) { 60 | final schemaInvocation = SchemaInvocation.fromInvocation(invocation); 61 | if (isInstance) { 62 | return noSuchInstanceMethod(schemaInvocation); 63 | } 64 | if (isQueryable) { 65 | return noSuchQueryMethod(schemaInvocation); 66 | } 67 | throw schemaInvocation; 68 | } 69 | 70 | @override 71 | String toString() { 72 | if (isInstance) { 73 | return toInstanceString(); 74 | } 75 | return super.toString(); 76 | } 77 | 78 | /// Validate the [data] and set it to the [instance] based on the 79 | /// [properties] defined in the [rootSchema]. 80 | static void setAll( 81 | Schema rootSchema, 82 | SchemaInstance instance, 83 | Map data, 84 | ) { 85 | for (final property in rootSchema.properties) { 86 | if (data.containsKey(property.fromKey)) { 87 | instance.set(property, data[property.fromKey]); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/data_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template data_store} 4 | /// A data store is ued for performing actions on the database for a 5 | /// given [Struct] and [Schema]. 6 | /// {@endtemplate} 7 | class DataStore { 8 | /// {@macro data_store} 9 | DataStore({ 10 | required Schema schema, 11 | required DataSource source, 12 | }) : assert(schema is T, 'schema must be of type $T'), 13 | _rootSchema = schema, 14 | _source = source; 15 | 16 | final Schema _rootSchema; 17 | 18 | final DataSource _source; 19 | 20 | DatabaseDriver get _driver => _source.driver; 21 | 22 | SchemaProperty get _primaryKey => _rootSchema.properties.firstWhere( 23 | (prop) => prop.isPrimary, 24 | ); 25 | 26 | /// Creates a new instance of the [T] type. 27 | /// 28 | /// This does not insert the instance into the database. You can use [save] 29 | /// for that. 30 | T create() => _createInstance(); 31 | 32 | /// Save or update the given data to the database. 33 | Future save(T data) async { 34 | final instance = data as SchemaInstance; 35 | final primary = instance.get(_primaryKey); 36 | 37 | if (primary == null) { 38 | await _driver.insert(_rootSchema, instance); 39 | } else { 40 | await _driver.update( 41 | _rootSchema, 42 | instance, 43 | query: Query([ 44 | [Where(_primaryKey, Operator.equals, primary.value)] 45 | ]), 46 | ); 47 | } 48 | 49 | // Everything is either inserted or updated, so lets save all the values. 50 | for (final prop in _rootSchema.properties) { 51 | instance.get(prop)?.persist(); 52 | } 53 | 54 | return data; 55 | } 56 | 57 | /// Delete the given data from the database. 58 | Future delete(T data) async { 59 | final instance = data as SchemaInstance; 60 | final primary = instance.get(_primaryKey); 61 | 62 | if (primary == null) { 63 | return; 64 | } 65 | 66 | await _driver.delete( 67 | _rootSchema, 68 | query: Query([ 69 | [Where(_primaryKey, Operator.equals, primary.value)] 70 | ]), 71 | ); 72 | } 73 | 74 | /// Create a query builder. 75 | QueryBuilder query() { 76 | return QueryBuilder(_rootSchema, (query) async { 77 | final result = await _driver.query(_rootSchema, query: query); 78 | return result.map((e) { 79 | final instance = _rootSchema.instance(); 80 | Schema.setAll(_rootSchema, instance, e); 81 | return instance as T; 82 | }).toList(); 83 | }); 84 | } 85 | 86 | /// Get a single instance by a given [key]. 87 | Future Function(V) getBy(V Function(T) key) { 88 | final invocation = SchemaInvocation(key, arguments: [_rootSchema as T]); 89 | 90 | return (V value) async { 91 | final result = await _driver.query( 92 | _rootSchema, 93 | query: Query([ 94 | [Where(_rootSchema.getProperty(invocation), Operator.equals, value)] 95 | ]), 96 | ); 97 | 98 | if (result.isEmpty) { 99 | return null; 100 | } 101 | return _createInstance(result.first); 102 | }; 103 | } 104 | 105 | T _createInstance([Map? data]) { 106 | final instance = _rootSchema.instance(); 107 | Schema.setAll(_rootSchema, instance, data ?? {}); 108 | return instance as T; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/wattles/test/src/data_store_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | int? id; 7 | 8 | late String key; 9 | } 10 | 11 | class _TestSchema extends Schema implements _TestStruct { 12 | _TestSchema() : super(_TestSchema.new, table: 'test') { 13 | assign(() => id, fromKey: 'id', isPrimary: true); 14 | assign(() => key, fromKey: 'key'); 15 | } 16 | } 17 | 18 | void main() { 19 | group('DataStore', () { 20 | late _TestSchema rootSchema; 21 | late DataStore<_TestStruct> dataStore; 22 | late DataSource dataSource; 23 | 24 | setUp(() { 25 | rootSchema = _TestSchema(); 26 | dataSource = DataSource.initialize( 27 | schemas: [rootSchema], 28 | driver: MemoryDriver(), 29 | ); 30 | dataStore = dataSource.getStore<_TestStruct>(); 31 | }); 32 | 33 | test('create', () { 34 | final instance = dataStore.create()..key = 'test'; 35 | 36 | expect(instance.id, isNull); 37 | expect(instance.key, equals('test')); 38 | }); 39 | 40 | group('save', () { 41 | test('saves a new record', () async { 42 | final instance = dataStore.create()..key = 'test'; 43 | 44 | await dataStore.save(instance); 45 | 46 | expect(instance.id, equals(1)); 47 | }); 48 | 49 | test('saves an existing record', () async { 50 | final instance = dataStore.create()..key = 'test'; 51 | 52 | await dataStore.save(instance); 53 | instance.key = 'new key'; 54 | 55 | expect( 56 | (instance as SchemaInstance) 57 | .get(rootSchema.properties.last)! 58 | .isModified, 59 | isTrue, 60 | ); 61 | 62 | await dataStore.save(instance); 63 | 64 | expect( 65 | (instance as SchemaInstance) 66 | .get(rootSchema.properties.last)! 67 | .isModified, 68 | isFalse, 69 | ); 70 | 71 | expect(instance.id, equals(1)); 72 | expect(instance.key, equals('new key')); 73 | }); 74 | }); 75 | 76 | group('delete', () { 77 | test('deletes a record', () async { 78 | final instance = dataStore.create()..key = 'test'; 79 | 80 | await dataStore.save(instance); 81 | await dataStore.delete(instance); 82 | 83 | expect(await dataStore.getBy((test) => test.id)(instance.id), isNull); 84 | }); 85 | }); 86 | 87 | group('getBy', () { 88 | test('returns a record', () async { 89 | final instance = dataStore.create()..key = 'test'; 90 | 91 | await dataStore.save(instance); 92 | 93 | final result = await dataStore.getBy((test) => test.key)(instance.key); 94 | 95 | expect(result?.id, equals(instance.id)); 96 | }); 97 | }); 98 | 99 | group('query', () { 100 | test('returns a list of records', () async { 101 | final instance1 = dataStore.create()..key = 'test'; 102 | final instance2 = dataStore.create()..key = 'test'; 103 | 104 | await dataStore.save(instance1); 105 | await dataStore.save(instance2); 106 | 107 | final query = dataStore.query() 108 | ..where((test) => test.key).equals('test'); 109 | 110 | final result = await query.getMany(); 111 | 112 | expect(result.length, equals(2)); 113 | expect(result.first.id, equals(instance1.id)); 114 | expect(result.last.id, equals(instance2.id)); 115 | }); 116 | }); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /packages/wattles/lib/src/drivers/memory_driver.dart: -------------------------------------------------------------------------------- 1 | import 'package:wattles/wattles.dart'; 2 | 3 | /// {@template sql_driver} 4 | /// Driver for storing data in memory. 5 | /// {@endtemplate} 6 | class MemoryDriver extends DatabaseDriver { 7 | final Map>> _data = {}; 8 | 9 | @override 10 | Future insert( 11 | Schema rootSchema, 12 | SchemaInstance instance, 13 | ) async { 14 | final collection = _data.putIfAbsent(rootSchema.table, () => []); 15 | // TODO(wolfen): should be handled more properly. 16 | final primaryKey = rootSchema.properties.firstWhere(_isPrimary); 17 | 18 | final fields = { 19 | for (final prop in rootSchema.properties.where(_isNotPrimary)) 20 | prop.fromKey: instance.get(prop)!, 21 | }; 22 | 23 | final map = fields.map((key, value) => MapEntry(key, value.value)); 24 | collection.add(map); 25 | 26 | final id = collection.length; 27 | 28 | instance.set(primaryKey, id); 29 | return map[primaryKey.fromKey] = id; 30 | } 31 | 32 | @override 33 | Future update( 34 | Schema rootSchema, 35 | SchemaInstance instance, { 36 | required Query query, 37 | }) async { 38 | final collection = _data.putIfAbsent(rootSchema.table, () => []); 39 | 40 | final records = _getRecords(collection, query); 41 | if (records.isEmpty) { 42 | return 0; 43 | } 44 | 45 | final fields = { 46 | for (final prop in rootSchema.properties.where(_isModified(instance))) 47 | prop.fromKey: instance.get(prop)!.value, 48 | }; 49 | if (fields.isEmpty) { 50 | return 0; 51 | } 52 | 53 | for (final record in records) { 54 | record.addAll(fields); 55 | } 56 | return records.length; 57 | } 58 | 59 | @override 60 | Future delete(Schema rootSchema, {required Query query}) async { 61 | final collection = _data.putIfAbsent(rootSchema.table, () => []); 62 | 63 | final records = _getRecords(collection, query); 64 | if (records.isEmpty) { 65 | return 0; 66 | } 67 | 68 | records.forEach(collection.remove); 69 | 70 | return records.length; 71 | } 72 | 73 | @override 74 | Future>> query( 75 | Schema rootSchema, { 76 | required Query query, 77 | }) async { 78 | final collection = _data.putIfAbsent(rootSchema.table, () => []); 79 | 80 | if (query.wheres.isEmpty) { 81 | return collection; 82 | } 83 | 84 | return _getRecords(collection, query); 85 | } 86 | 87 | /// Check if given property is a primary key. 88 | bool _isPrimary(SchemaProperty prop) => prop.isPrimary; 89 | 90 | /// Check if given property is not a primary key. 91 | bool _isNotPrimary(SchemaProperty prop) => !prop.isPrimary; 92 | 93 | /// Check if given property is modified. 94 | bool Function(SchemaProperty) _isModified(SchemaInstance instance) => 95 | (prop) => instance.get(prop)?.isModified ?? false; 96 | 97 | List> _getRecords( 98 | List> collection, 99 | Query query, 100 | ) { 101 | final records = collection.where((e) { 102 | return query.wheres.any( 103 | (wheres) => wheres.every((where) => where.check(e)), 104 | ); 105 | }).toList(); 106 | 107 | return records.sublist(0, query.limit ?? records.length); 108 | } 109 | } 110 | 111 | extension on Where { 112 | bool check(Map data) { 113 | final dataValue = data[property.fromKey]; 114 | switch (operator) { 115 | case Operator.equals: 116 | return dataValue == value; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/wattles/test/src/drivers/memory_driver_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:test/test.dart'; 3 | import 'package:wattles/wattles.dart'; 4 | 5 | abstract class _TestStruct extends Struct { 6 | int? id; 7 | 8 | late String key; 9 | } 10 | 11 | class _TestSchema extends Schema implements _TestStruct { 12 | _TestSchema() : super(_TestSchema.new, table: 'test') { 13 | assign(() => id, fromKey: 'id', isPrimary: true); 14 | assign(() => key, fromKey: 'key'); 15 | } 16 | } 17 | 18 | void main() { 19 | group('MemoryDriver', () { 20 | late _TestSchema rootSchema; 21 | late DatabaseDriver driver; 22 | 23 | setUp(() { 24 | rootSchema = _TestSchema(); 25 | driver = MemoryDriver(); 26 | }); 27 | 28 | group('insert', () { 29 | test('inserts a record', () async { 30 | final instance1 = rootSchema.instance(); 31 | (instance1 as _TestStruct).key = 'key'; 32 | 33 | final instance2 = rootSchema.instance(); 34 | (instance2 as _TestStruct).key = 'other key'; 35 | 36 | await driver.insert(rootSchema, instance1); 37 | await driver.insert(rootSchema, instance2); 38 | 39 | expect((instance1 as _TestStruct).id, equals(1)); 40 | expect((instance2 as _TestStruct).id, equals(2)); 41 | }); 42 | }); 43 | 44 | group('update', () { 45 | late _TestStruct instance; 46 | late SchemaProperty primaryKey; 47 | 48 | setUp(() async { 49 | instance = (rootSchema.instance() as _TestStruct)..key = 'key'; 50 | await driver.insert( 51 | rootSchema, 52 | instance as SchemaInstance, 53 | ); 54 | 55 | primaryKey = rootSchema.properties.firstWhere((prop) => prop.isPrimary); 56 | }); 57 | 58 | test('updates a record', () async { 59 | instance.key = 'new key'; 60 | 61 | final result = await driver.update( 62 | rootSchema, 63 | instance as SchemaInstance, 64 | query: Query([ 65 | [Where(primaryKey, Operator.equals, instance.id)] 66 | ]), 67 | ); 68 | 69 | expect(result, equals(1)); 70 | expect(instance.id, equals(1)); 71 | expect(instance.key, equals('new key')); 72 | }); 73 | 74 | test('updates no records if none are found', () async { 75 | final result = await driver.update( 76 | rootSchema, 77 | instance as SchemaInstance, 78 | query: Query([ 79 | [Where(primaryKey, Operator.equals, 2)] 80 | ]), 81 | ); 82 | 83 | expect(result, equals(0)); 84 | expect(instance.id, equals(1)); 85 | expect(instance.key, equals('key')); 86 | }); 87 | 88 | test('updates no records if no changes are found', () async { 89 | final result = await driver.update( 90 | rootSchema, 91 | instance as SchemaInstance, 92 | query: Query([ 93 | [Where(primaryKey, Operator.equals, instance.id)] 94 | ]), 95 | ); 96 | 97 | expect(result, equals(0)); 98 | expect(instance.id, equals(1)); 99 | expect(instance.key, equals('key')); 100 | }); 101 | }); 102 | 103 | group('delete', () { 104 | setUp(() async { 105 | final first = (rootSchema.instance() as _TestStruct)..key = 'some'; 106 | final second = (rootSchema.instance() as _TestStruct)..key = 'value'; 107 | final third = (rootSchema.instance() as _TestStruct)..key = 'some'; 108 | 109 | await driver.insert(rootSchema, first as SchemaInstance); 110 | await driver.insert(rootSchema, second as SchemaInstance); 111 | await driver.insert(rootSchema, third as SchemaInstance); 112 | }); 113 | 114 | test('returns all records', () async { 115 | final result = await driver.query(rootSchema, query: Query(const [])); 116 | 117 | expect(result.length, equals(3)); 118 | expect(result[0]['id'], equals(1)); 119 | expect(result[0]['key'], equals('some')); 120 | expect(result[1]['id'], equals(2)); 121 | expect(result[1]['key'], equals('value')); 122 | expect(result[2]['id'], equals(3)); 123 | expect(result[2]['key'], equals('some')); 124 | }); 125 | 126 | test('delete a record', () async { 127 | final result = await driver.delete( 128 | rootSchema, 129 | query: Query([ 130 | [Where(rootSchema.properties.last, Operator.equals, 'value')] 131 | ]), 132 | ); 133 | 134 | expect(result, equals(1)); 135 | 136 | expect( 137 | (await driver.query(rootSchema, query: Query(const []))).length, 138 | equals(2), 139 | ); 140 | }); 141 | 142 | test('delete multiple records', () async { 143 | final result = await driver.delete( 144 | rootSchema, 145 | query: Query([ 146 | [Where(rootSchema.properties.last, Operator.equals, 'some')] 147 | ]), 148 | ); 149 | 150 | expect(result, equals(2)); 151 | 152 | expect( 153 | (await driver.query(rootSchema, query: Query(const []))).length, 154 | equals(1), 155 | ); 156 | }); 157 | 158 | test('delete no records if none are found', () async { 159 | final result = await driver.delete( 160 | rootSchema, 161 | query: Query([ 162 | [Where(rootSchema.properties.last, Operator.equals, 'none')] 163 | ]), 164 | ); 165 | 166 | expect(result, equals(0)); 167 | 168 | expect( 169 | (await driver.query(rootSchema, query: Query(const []))).length, 170 | equals(3), 171 | ); 172 | }); 173 | }); 174 | 175 | group('query', () { 176 | setUp(() async { 177 | final first = (rootSchema.instance() as _TestStruct)..key = 'some'; 178 | final second = (rootSchema.instance() as _TestStruct)..key = 'value'; 179 | final third = (rootSchema.instance() as _TestStruct)..key = 'some'; 180 | 181 | await driver.insert(rootSchema, first as SchemaInstance); 182 | await driver.insert(rootSchema, second as SchemaInstance); 183 | await driver.insert(rootSchema, third as SchemaInstance); 184 | }); 185 | 186 | test('returns all records', () async { 187 | final result = await driver.query(rootSchema, query: Query(const [])); 188 | 189 | expect(result.length, equals(3)); 190 | expect(result[0]['id'], equals(1)); 191 | expect(result[0]['key'], equals('some')); 192 | expect(result[1]['id'], equals(2)); 193 | expect(result[1]['key'], equals('value')); 194 | expect(result[2]['id'], equals(3)); 195 | expect(result[2]['key'], equals('some')); 196 | }); 197 | 198 | test('returns records that match the query', () async { 199 | final result = await driver.query( 200 | rootSchema, 201 | query: Query([ 202 | [Where(rootSchema.properties.last, Operator.equals, 'some')] 203 | ]), 204 | ); 205 | 206 | expect(result.length, equals(2)); 207 | expect(result[0]['id'], equals(1)); 208 | expect(result[0]['key'], equals('some')); 209 | expect(result[1]['id'], equals(3)); 210 | expect(result[1]['key'], equals('some')); 211 | }); 212 | 213 | test('returns records that match the query with a limit', () async { 214 | final result = await driver.query( 215 | rootSchema, 216 | query: Query( 217 | [ 218 | [Where(rootSchema.properties.last, Operator.equals, 'some')] 219 | ], 220 | limit: 1, 221 | ), 222 | ); 223 | 224 | expect(result.length, equals(1)); 225 | expect(result[0]['id'], equals(1)); 226 | expect(result[0]['key'], equals('some')); 227 | }); 228 | }); 229 | }); 230 | } 231 | --------------------------------------------------------------------------------