diff --git a/.gitignore b/.gitignore index 2509316..280bdea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .vscode .dart_tool/ .idea +.nvimrc pubspec_overrides.yaml melos_dartseid.iml .DS_Store +.fvm +.nvim.lua diff --git a/.nvim.lua b/.nvim.lua new file mode 100644 index 0000000..ebe4aff --- /dev/null +++ b/.nvim.lua @@ -0,0 +1,2 @@ +vim.opt.tabstop = 2 +vim.opt.shiftwidth = 2 diff --git a/melos.yaml b/melos.yaml index a127d57..75667fa 100644 --- a/melos.yaml +++ b/melos.yaml @@ -7,4 +7,16 @@ packages: scripts: analyze: run: dart analyze + + format: + exec: dart format lib + test: + exec: dart test + packageFilters: + scope: '*' + + testArcadeOrm: + exec: dart test --reporter=expanded --chain-stack-traces + packageFilters: + scope: arcade_orm diff --git a/melos_arcade.iml b/melos_arcade.iml index 097e074..257c87f 100644 --- a/melos_arcade.iml +++ b/melos_arcade.iml @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/packages/arcade/.gitignore b/packages/arcade/.gitignore index f0c1008..344541d 100644 --- a/packages/arcade/.gitignore +++ b/packages/arcade/.gitignore @@ -8,5 +8,6 @@ pubspec.lock .idea/ +melos_arcade.iml # Generated files in example example/**/*.g.dart diff --git a/packages/arcade/melos_arcade.iml b/packages/arcade/melos_arcade.iml deleted file mode 100644 index 389d07a..0000000 --- a/packages/arcade/melos_arcade.iml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/arcade_orm/.gitignore b/packages/arcade_orm/.gitignore new file mode 100644 index 0000000..ad54996 --- /dev/null +++ b/packages/arcade_orm/.gitignore @@ -0,0 +1,9 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +melos_arcade_orm.iml diff --git a/packages/arcade_orm/CHANGELOG.md b/packages/arcade_orm/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/arcade_orm/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/arcade_orm/README.md b/packages/arcade_orm/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/packages/arcade_orm/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/arcade_orm/analysis_options.yaml b/packages/arcade_orm/analysis_options.yaml new file mode 100644 index 0000000..55309e9 --- /dev/null +++ b/packages/arcade_orm/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lint/analysis_options.yaml diff --git a/packages/arcade_orm/example/api/basic.dart b/packages/arcade_orm/example/api/basic.dart new file mode 100644 index 0000000..73c887c --- /dev/null +++ b/packages/arcade_orm/example/api/basic.dart @@ -0,0 +1,208 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/include.dart'; +import 'package:arcade_orm/src/query/mixins/sort.dart'; +import 'package:arcade_orm/src/query/select.dart'; +import 'package:arcade_orm/src/query/where.dart'; + +typedef OptionsRecord = ({String? name, String? host, int? port}); + +class ArcadeOrmMockAdapter + implements ArcadeOrmAdapterBase { + @override + late ArcadeOrm orm; + @override + final OptionsRecord? options; + + @override + final String connection; + + ArcadeOrmMockAdapter({ + required this.connection, + this.options, + }); + + @override + Future init() { + // TODO: implement init + throw UnimplementedError(); + } + + @override + ArcadeOrmTransaction transaction() { + // TODO: implement transaction + throw UnimplementedError(); + } + + @override + void setArcadeOrmInstance(ArcadeOrm orm) { + this.orm = orm; + } + + @override + FutureOr close() { + // TODO: implement close + throw UnimplementedError(); + } + + @override + Future> operate( + {required TableOperator operator, + required ArcadeOrmTransaction? transaction, + required bool isVerbose, + String? rawSql, + List>? rawNoSqlAggregate, + Map? rawNoSqlAggregateOptions, + Map? rawParams, + WhereExpressionNode? whereParams, + WhereExpressionNode? havingParams, + List> selectParams = const [], + List includeParams = const [], + List groupParams = const [], + List> sortParams = const [], + Map updateWithParams = const {}, + List> insertWithParams = const [], + int? limit, + int? skip}) { + // TODO: implement operate + throw UnimplementedError(); + } +} + +class UserTable extends ArcadeOrmTableSchema { + UserTable(super.orm); + + @override + final String tableName = "user"; + + static const String id = "id"; + static const String name = "name"; + static const String email = "email"; + static const String age = "age"; + static const String profileId = "profileId"; + + @override + Map schema = const { + id: ColumnInt(), + name: ColumnString(), + email: ColumnString(), + age: ColumnInt(), + profileId: ColumnInt(), + }; + + @override + Map index = { + name: (1, unique: null), + }; + + @override + Map relations = const { + "profile": ( + type: ArcadeOrmRelationshipType.hasOne, + table: ProfileTable.$tableName, + localKey: profileId, + foreignKey: ProfileTable.id, + ), + }; +} + +class ProfileTable extends ArcadeOrmTableSchema { + ProfileTable(super.orm); + + static const String $tableName = "profile"; + + @override + String tableName = $tableName; + + static const String id = "id"; + static const String bio = "bio"; + + @override + Map schema = {}; +} + +late final UserTable userTable; +late final ProfileTable profileTable; + +Future orming() async { + final orm = await ArcadeOrm.init( + adapter: ArcadeOrmMockAdapter(connection: ""), + ); + + userTable = UserTable(orm); + profileTable = ProfileTable(orm); + + final trx = userTable.transaction(); + await trx.start(); + userTable.findOne(transaction: trx); + trx.commit(); + trx.rollback(); + + final data = await userTable.transaction().start( + (trx) async { + final r = userTable.findOne(transaction: trx) + ..where({ + UserTable.id: inList([1, 2, 4]), + }) + ..where({UserTable.id: eq(10)}) + ..where( + and([ + { + UserTable.name: like("%aa"), + UserTable.id: inList([1, 2, 4]), + }, + {UserTable.name: eq("2")}, + ]), + ) + ..where( + or([ + { + UserTable.name: like("%aa"), + UserTable.id: between(1, 200), + } + ]), + ) + ..select({ + UserTable.name: show(), + UserTable.id: hide(), + "nom": field(UserTable.name), + "avg": avg(UserTable.age), + "count": count(UserTable.name), + "countD": count("DISTINCT(${UserTable.name})"), + "countD2": countDistinct(UserTable.name), + "x": distinct(UserTable.name), + "max": max(UserTable.age), + }) + ..include( + profileTable, + on: UserTable.profileId, + where: and([ + {ProfileTable.bio: notEq(null)}, + {ProfileTable.bio: notEq("")}, + ]), + joinType: JoinOperation.left, + ) + ..group(UserTable.id) + ..sort({UserTable.name: SortDirection.asc}) + ..limit(10) + ..skip(0); + + Future fromJsonFn(Map j) async { + return ""; + } + + final result = await r.exec(fromJson: fromJsonFn); + + return switch (result) { + ExecResultData(data: final data) => data, + ExecResultFailure(exception: final _) => await () async { + throw Exception("I am basic"); + }() + }; + }, + ); + + // more orming + return data; +} diff --git a/packages/arcade_orm/lib/arcade_orm.dart b/packages/arcade_orm/lib/arcade_orm.dart new file mode 100644 index 0000000..531e433 --- /dev/null +++ b/packages/arcade_orm/lib/arcade_orm.dart @@ -0,0 +1,4 @@ +export 'src/adapter.dart'; +export 'src/core.dart'; +export 'src/query.dart'; +export 'src/table.dart'; diff --git a/packages/arcade_orm/lib/src/adapter.dart b/packages/arcade_orm/lib/src/adapter.dart new file mode 100644 index 0000000..fafb31a --- /dev/null +++ b/packages/arcade_orm/lib/src/adapter.dart @@ -0,0 +1,137 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/include.dart'; +import 'package:arcade_orm/src/query/mixins/sort.dart'; +import 'package:arcade_orm/src/query/select.dart'; +import 'package:arcade_orm/src/query/where.dart'; +import 'package:meta/meta.dart'; + +abstract interface class ArcadeOrmAdapterBase { + @protected + final U connection; + @protected + final T? options; + @protected + late ArcadeOrm orm; + + ArcadeOrmAdapterBase({ + required this.connection, + this.options, + }); + + FutureOr init(); + Future> operate({ + required TableOperator operator, + required ArcadeOrmTransaction? transaction, + required bool isVerbose, + String? rawSql, + List>? rawNoSqlAggregate, + Map? rawNoSqlAggregateOptions, + Map? rawParams, + WhereExpressionNode? whereParams, + WhereExpressionNode? havingParams, + List> selectParams = const [], + List includeParams = const [], + List groupParams = const [], + List> sortParams = const [], + Map updateWithParams = const {}, + List> insertWithParams = const [], + int? limit, + int? skip, + }); + + void setArcadeOrmInstance(ArcadeOrm orm); + + ArcadeOrmTransaction transaction(); + + FutureOr close(); +} + +abstract class ArcadeOrmTransaction { + bool _isStarted = false; + bool _isCommitted = false; + bool _isRolledBack = false; + + bool get isStarted => _isStarted; + bool get isComitted => _isCommitted; + bool get isRolledBack => _isRolledBack; + + Future commit() async { + _checkPrecondition("Commit"); + await $commit(); + _isCommitted = true; + } + + Future rollback() async { + _checkPrecondition("Rollback"); + await $rollback(); + _isRolledBack = true; + } + + Future start([ + FutureOr Function(ArcadeOrmTransaction trx)? callback, + ]) async { + try { + _checkStartPreCondition(); + await $startTransaction(); + _isStarted = true; + if (callback == null) { + return null; + } + final data = await callback(this); + await commit(); + return data; + } catch (e) { + if (callback != null) { + await rollback(); + } + rethrow; + } + } + + @protected + Future $commit(); + + @protected + Future $rollback(); + + @protected + Future $startTransaction(); + + void _checkStartPreCondition() { + if (_isCommitted) { + throw ArcadeOrmException( + message: "Cannot Start a transaction that is already committed", + originalError: null, + ); + } + if (_isRolledBack) { + throw ArcadeOrmException( + message: "Cannot Start a transaction that is already rolled back", + originalError: null, + ); + } + } + + void _checkPrecondition(String op) { + if (!_isStarted) { + throw ArcadeOrmException( + message: "Cannot $op a transaction that has not been started", + originalError: null, + ); + } + if (_isCommitted) { + throw ArcadeOrmException( + message: "Cannot $op a transaction that is already committed", + originalError: null, + ); + } + if (_isRolledBack) { + throw ArcadeOrmException( + message: "Cannot $op a transaction that has been rolled back", + originalError: null, + ); + } + } +} diff --git a/packages/arcade_orm/lib/src/core.dart b/packages/arcade_orm/lib/src/core.dart new file mode 100644 index 0000000..f511a7f --- /dev/null +++ b/packages/arcade_orm/lib/src/core.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:meta/meta.dart'; + +@visibleForTesting +void clearOrms() { + _orms.clear(); +} + +// This will be initialized with dart orm class after init method is called +final Map _orms = {}; + +/// This will return an instance of ArcadeOrm provided `init` was called +ArcadeOrm getArcadeOrmInstance([String? name]) { + final orm = _orms[name ?? "__default"]; + if (orm == null) { + throw StateError( + 'Arcade orm is not initialized. ' + 'Please run ArcadeOrm.init(adapter: ExampleAdapter()${name != null ? ', name: "$name"' : ''})', + ); + } + return orm; +} + +class ArcadeOrm with ArcadeOrmTable { + final ArcadeOrmAdapterBase adapter; + + ArcadeOrm._({required this.adapter, String? name}) { + this.name = name ?? "__default"; + if (_orms.containsKey(this.name)) { + throw StateError( + 'Arcade ORM with name "${this.name}" is already initialized. Please use another name', + ); + } + _orms[this.name] = this; + } + + ArcadeOrmTransaction transaction() { + return adapter.transaction(); + } + + static FutureOr init({ + required ArcadeOrmAdapterBase adapter, + String? name, + }) async { + final orm = ArcadeOrm._(adapter: adapter, name: name); + await adapter.init(); + adapter.setArcadeOrmInstance(orm); + return orm; + } + + FutureOr close() async { + await adapter.close(); + } +} + +class ArcadeOrmException implements Exception { + final Object? originalError; + final String message; + + ArcadeOrmException({required this.message, required this.originalError}); +} diff --git a/packages/arcade_orm/lib/src/query.dart b/packages/arcade_orm/lib/src/query.dart new file mode 100644 index 0000000..112397e --- /dev/null +++ b/packages/arcade_orm/lib/src/query.dart @@ -0,0 +1,246 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/mixins/group.dart'; +import 'package:arcade_orm/src/query/mixins/include.dart'; +import 'package:arcade_orm/src/query/mixins/insert.dart'; +import 'package:arcade_orm/src/query/mixins/pagination.dart'; +import 'package:arcade_orm/src/query/mixins/raw.dart'; +import 'package:arcade_orm/src/query/mixins/select.dart'; +import 'package:arcade_orm/src/query/mixins/sort.dart'; +import 'package:arcade_orm/src/query/mixins/update.dart'; +import 'package:arcade_orm/src/query/mixins/verbose.dart'; +import 'package:arcade_orm/src/query/mixins/where.dart'; + +class ArcadeOrmTableFindOperator + with + SelectMixin, + WhereMixin, + GroupMixin, + PaginationMixin, + SortMixin, + IncludeMixin, + VerboseMixin { + final ArcadeOrm _orm; + final TableOperator _operator; + final ArcadeOrmTransaction? _transaction; + + ArcadeOrmTableFindOperator({ + required ArcadeOrm orm, + required TableOperator operator, + ArcadeOrmTransaction? transaction, + }) : _transaction = transaction, + _operator = operator, + _orm = orm; + + Future> exec({ + ArcadeOrmTableFromConverter? fromJson, + }) async { + try { + final data = await _orm.adapter.operate( + isVerbose: $verbose, + operator: _operator, + transaction: _transaction, + whereParams: $whereParams?.simplify(), + includeParams: $includeParams, + selectParams: $selectParams, + // havingParams: _havingParams, + groupParams: $groupParams, + sortParams: $sortParams, + limit: $limit, + skip: $skip, + ); + + final convertedData = await fromJson?.call(data); + + return ExecResultData(convertedData ?? data as T); + } on ArcadeOrmException catch (e) { + return ExecResultFailure(e); + } catch (e) { + return ExecResultFailure( + ArcadeOrmException(originalError: e, message: "Unknown Error"), + ); + } + } + + // void having(Map map) { + // _whereParams.add(WhereParam.evaluateWhereParams(map)); + // } +} + +class ArcadeOrmTableInsertOperator with InsertMixin, VerboseMixin { + final ArcadeOrm _orm; + final TableOperator _operator; + final ArcadeOrmTransaction? _transaction; + + ArcadeOrmTableInsertOperator({ + required ArcadeOrm orm, + required TableOperator operator, + ArcadeOrmTransaction? transaction, + }) : _transaction = transaction, + _operator = operator, + _orm = orm; + + Future exec({ + ArcadeOrmTableFromConverter? fromJson, + }) async { + try { + final data = await _orm.adapter.operate( + isVerbose: $verbose, + operator: _operator, + transaction: _transaction, + insertWithParams: $insertWithParams, + ); + + final convertedData = await fromJson?.call(data); + + return ExecResultData(convertedData ?? data); + } on ArcadeOrmException catch (e) { + return ExecResultFailure(e); + } catch (e) { + return ExecResultFailure( + ArcadeOrmException(originalError: e, message: "Unknown Error"), + ); + } + } +} + +class ArcadeOrmTableDeleteOperator with WhereMixin, IncludeMixin, VerboseMixin { + final ArcadeOrm _orm; + final TableOperator _operator; + final ArcadeOrmTransaction? _transaction; + + ArcadeOrmTableDeleteOperator({ + required ArcadeOrm orm, + required TableOperator operator, + ArcadeOrmTransaction? transaction, + }) : _transaction = transaction, + _operator = operator, + _orm = orm; + + Future exec({ + ArcadeOrmTableFromConverter? fromJson, + }) async { + try { + final data = await _orm.adapter.operate( + operator: _operator, + transaction: _transaction, + isVerbose: $verbose, + whereParams: $whereParams?.simplify(), + includeParams: $includeParams, + ); + + final convertedData = await fromJson?.call(data); + + return ExecResultData(convertedData ?? data); + } on ArcadeOrmException catch (e) { + return ExecResultFailure(e); + } catch (e) { + return ExecResultFailure( + ArcadeOrmException(originalError: e, message: "Unknown Error"), + ); + } + } +} + +class ArcadeOrmTableRawOperator with VerboseMixin, RawMixin { + final ArcadeOrm _orm; + final TableOperator _operator; + final ArcadeOrmTransaction? _transaction; + + ArcadeOrmTableRawOperator({ + required ArcadeOrm orm, + required TableOperator operator, + ArcadeOrmTransaction? transaction, + }) : _transaction = transaction, + _operator = operator, + _orm = orm; + + Future exec({ + ArcadeOrmTableFromConverter? fromJson, + }) async { + try { + final data = await _orm.adapter.operate( + rawSql: $rawSql, + rawParams: $params, + rawNoSqlAggregate: $rawNoSqlAggregate, + rawNoSqlAggregateOptions: $rawNoSqlAggregateOptions, + isVerbose: $verbose, + operator: _operator, + transaction: _transaction, + ); + + final convertedData = await fromJson?.call(data); + + return ExecResultData(convertedData ?? data); + } on ArcadeOrmException catch (e) { + return ExecResultFailure(e); + } catch (e) { + return ExecResultFailure( + ArcadeOrmException(originalError: e, message: "Unknown Error"), + ); + } + } +} + +class ArcadeOrmTableUpdateOperator with WhereMixin, UpdateMixin, VerboseMixin { + final ArcadeOrm _orm; + final TableOperator _operator; + final ArcadeOrmTransaction? _transaction; + + ArcadeOrmTableUpdateOperator({ + required ArcadeOrm orm, + required TableOperator operator, + ArcadeOrmTransaction? transaction, + }) : _transaction = transaction, + _operator = operator, + _orm = orm; + + Future exec({ + ArcadeOrmTableFromConverter? fromJson, + }) async { + try { + final data = await _orm.adapter.operate( + isVerbose: $verbose, + operator: _operator, + transaction: _transaction, + whereParams: $whereParams?.simplify(), + updateWithParams: $updateWithParams, + ); + + final convertedData = await fromJson?.call(data); + + return ExecResultData(convertedData ?? data); + } on ArcadeOrmException catch (e) { + return ExecResultFailure(e); + } catch (e) { + return ExecResultFailure( + ArcadeOrmException(originalError: e, message: "Unknown Error"), + ); + } + } +} + +sealed class ExecResult {} + +class ExecResultData extends ExecResult { + final T data; + + ExecResultData(this.data); +} + +class ExecResultFailure extends ExecResult { + final ArcadeOrmException exception; + + ExecResultFailure(this.exception); +} + +enum TableOperator { + raw, + count, + findOne, + findMany, + insert, + update, + delete, +} diff --git a/packages/arcade_orm/lib/src/query/include.dart b/packages/arcade_orm/lib/src/query/include.dart new file mode 100644 index 0000000..2c65406 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/include.dart @@ -0,0 +1,31 @@ +import 'package:arcade_orm/src/query/where.dart'; + +enum JoinOperation { + inner, + left, + right, + cross, +} + +class IncludeParam { + final String tableName; + final String? on; + final String? as; + final JoinOperation joinType; + WhereExpressionNode? where; + IncludeParam( + this.tableName, { + this.on, + this.as, + this.joinType = JoinOperation.inner, + Map? where, + }) { + final value = WhereParamBuilder.root(null); + if (where != null) { + for (final element in where.entries) { + value.addNode(element.value, element.key); + } + this.where = value.simplify(); + } + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/group.dart b/packages/arcade_orm/lib/src/query/mixins/group.dart new file mode 100644 index 0000000..585f37b --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/group.dart @@ -0,0 +1,9 @@ +import 'package:meta/meta.dart'; + +mixin GroupMixin { + @protected + final List $groupParams = []; + void group(String column) { + $groupParams.add(column); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/include.dart b/packages/arcade_orm/lib/src/query/mixins/include.dart new file mode 100644 index 0000000..13ec1f2 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/include.dart @@ -0,0 +1,27 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/include.dart'; +import 'package:arcade_orm/src/query/where.dart'; +import 'package:meta/meta.dart'; + +mixin IncludeMixin { + @protected + final List $includeParams = []; + + void include( + ArcadeOrmTableSchema table, { + String? on, + String? as, + Map? where, + JoinOperation joinType = JoinOperation.inner, + }) { + $includeParams.add( + IncludeParam( + table.tableName, + on: on, + as: as, + where: where, + joinType: joinType, + ), + ); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/insert.dart b/packages/arcade_orm/lib/src/query/mixins/insert.dart new file mode 100644 index 0000000..41d2f96 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/insert.dart @@ -0,0 +1,10 @@ +import 'package:meta/meta.dart'; + +mixin InsertMixin { + @protected + final List> $insertWithParams = []; + + void insertWith(Map value) { + $insertWithParams.add(value); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/pagination.dart b/packages/arcade_orm/lib/src/query/mixins/pagination.dart new file mode 100644 index 0000000..4da1c7a --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/pagination.dart @@ -0,0 +1,31 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:meta/meta.dart'; + +mixin PaginationMixin { + @protected + int? $limit; + @protected + int? $skip; + + /// Set the number of results skipped. Cannot be less than 0. + void skip(int skip) { + if (skip < 0) { + throw ArcadeOrmException( + message: "skip cannot be less then 0", + originalError: null, + ); + } + $skip = skip; + } + + /// Set the number of results to return. Cannot be less than 1. + void limit(int limit) { + if (limit <= 0) { + throw ArcadeOrmException( + message: "limit cannot be less then 1", + originalError: null, + ); + } + $limit = limit; + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/raw.dart b/packages/arcade_orm/lib/src/query/mixins/raw.dart new file mode 100644 index 0000000..a65af28 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/raw.dart @@ -0,0 +1,27 @@ +import 'package:meta/meta.dart'; + +mixin RawMixin { + @protected + String? $rawSql; + @protected + Map? $params; + @protected + List>? $rawNoSqlAggregate; + @protected + Map? $rawNoSqlAggregateOptions; + + // ignore: use_setters_to_change_properties + void aggregate( + List>? aggregate, { + Map? options, + }) { + $rawNoSqlAggregate = aggregate; + $rawNoSqlAggregateOptions = options; + } + + // ignore: use_setters_to_change_properties + void sql(String query, {Map? params}) { + $rawSql = query; + $params = params; + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/select.dart b/packages/arcade_orm/lib/src/query/mixins/select.dart new file mode 100644 index 0000000..234b1ab --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/select.dart @@ -0,0 +1,10 @@ +import 'package:arcade_orm/src/query/select.dart'; +import 'package:meta/meta.dart'; + +mixin SelectMixin { + @protected + final List> $selectParams = []; + void select(Map value) { + $selectParams.add(value); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/sort.dart b/packages/arcade_orm/lib/src/query/mixins/sort.dart new file mode 100644 index 0000000..b180113 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/sort.dart @@ -0,0 +1,11 @@ +enum SortDirection { asc, desc } + +mixin SortMixin { + final List> $sortParams = []; + + void sort(Map sortBy) { + final Map sortValue = + sortBy.map((key, value) => MapEntry(key, value)); + $sortParams.add(sortValue); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/update.dart b/packages/arcade_orm/lib/src/query/mixins/update.dart new file mode 100644 index 0000000..fa6a632 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/update.dart @@ -0,0 +1,10 @@ +import 'package:meta/meta.dart'; + +mixin UpdateMixin { + @protected + final Map $updateWithParams = {}; + + void updateWith(Map value) { + $updateWithParams.addAll(value); + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/verbose.dart b/packages/arcade_orm/lib/src/query/mixins/verbose.dart new file mode 100644 index 0000000..ae80f8a --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/verbose.dart @@ -0,0 +1,9 @@ +import 'package:meta/meta.dart'; + +mixin VerboseMixin { + @protected + bool $verbose = false; + void verbose() { + $verbose = true; + } +} diff --git a/packages/arcade_orm/lib/src/query/mixins/where.dart b/packages/arcade_orm/lib/src/query/mixins/where.dart new file mode 100644 index 0000000..1f2b097 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/mixins/where.dart @@ -0,0 +1,14 @@ +import 'package:arcade_orm/src/query/where.dart'; +import 'package:meta/meta.dart'; + +mixin WhereMixin { + @protected + WhereExpressionOperatorNode? $whereParams; + + void where(Map map) { + $whereParams ??= WhereParamBuilder.root(null); + for (final entry in map.entries) { + $whereParams!.addNode(entry.value, entry.key); + } + } +} diff --git a/packages/arcade_orm/lib/src/query/select.dart b/packages/arcade_orm/lib/src/query/select.dart new file mode 100644 index 0000000..582e19c --- /dev/null +++ b/packages/arcade_orm/lib/src/query/select.dart @@ -0,0 +1,89 @@ +enum SelectAggregationOperator { + show, + hide, + count, + sum, + avg, + min, + max, + distinct, + countDistinct, +} + +class SelectParam { + String? fieldAs; + final SelectAggregationOperator operator; + + SelectParam({ + required this.operator, + this.fieldAs, + }); +} + +SelectParam avg(String value) { + return SelectParam( + operator: SelectAggregationOperator.avg, + fieldAs: value, + ); +} + +SelectParam count(String value) { + return SelectParam( + operator: SelectAggregationOperator.count, + fieldAs: value, + ); +} + +SelectParam countDistinct(String value) { + return SelectParam( + operator: SelectAggregationOperator.countDistinct, + fieldAs: value, + ); +} + +SelectParam distinct(String value) { + return SelectParam( + operator: SelectAggregationOperator.distinct, + fieldAs: value, + ); +} + +SelectParam field(String value) { + return SelectParam( + operator: SelectAggregationOperator.show, + fieldAs: value, + ); +} + +SelectParam hide() { + return SelectParam( + operator: SelectAggregationOperator.hide, + ); +} + +SelectParam max(String value) { + return SelectParam( + operator: SelectAggregationOperator.max, + fieldAs: value, + ); +} + +SelectParam min(String value) { + return SelectParam( + operator: SelectAggregationOperator.min, + fieldAs: value, + ); +} + +SelectParam show() { + return SelectParam( + operator: SelectAggregationOperator.show, + ); +} + +SelectParam sum(String value) { + return SelectParam( + operator: SelectAggregationOperator.sum, + fieldAs: value, + ); +} diff --git a/packages/arcade_orm/lib/src/query/where.dart b/packages/arcade_orm/lib/src/query/where.dart new file mode 100644 index 0000000..dc05576 --- /dev/null +++ b/packages/arcade_orm/lib/src/query/where.dart @@ -0,0 +1,308 @@ +const dAnd = r'$__AND'; +const dOr = r'$__OR'; + +enum WhereExpressionOperator { + and(dAnd), + or(dOr); + + final String name; + + const WhereExpressionOperator(this.name); +} + +sealed class WhereParamBuilder { + WhereParamBuilder(); + + // ignore: prefer_constructors_over_static_methods + static WhereExpressionOperatorNode root(WhereExpressionOperatorNode? node) { + return node ?? + WhereExpressionOperatorNode( + operator: WhereExpressionOperator.and, + nodes: [], + ); + } +} + +class WhereParam extends WhereParamBuilder { + final WhereOperator operator; + final T? value; + final T? start; + final T? end; + + WhereParam({ + required this.operator, + this.value, + this.start, + this.end, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is WhereParam && + other.operator == operator && + other.value == value && + other.start == start && + other.end == end; + + @override + int get hashCode => Object.hash(operator, value, start, end); + + @override + String toString() { + return 'WhereParam{operator: $operator, value: $value, start: $start, end: $end}'; + } + + String toJson() { + return toString(); + } +} + +sealed class WhereExpressionNode extends WhereParamBuilder { + List nodes; + + WhereExpressionNode({ + this.nodes = const [], + }); + + void addNode( + WhereParamBuilder node, [ + String? field, + ]) { + if (this is WhereExpressionCompareNode) { + throw StateError('Cannot add node to compare node'); + } + if (this is WhereParam) { + throw StateError('Cannot add node to where param'); + } + if (this is WhereExpressionOperatorNode) { + if (node is WhereExpressionOperatorNode) { + if (node.operator == (this as WhereExpressionOperatorNode).operator) { + nodes.addAll(node.nodes); + return; + } + nodes.add(node); + return; + } + if (node is WhereExpressionCompareNode) { + nodes.add(node); + return; + } + if (node is WhereParam) { + if (field == null) { + throw StateError('Field cannot be null for WhereParam'); + } + nodes.add( + WhereExpressionCompareNode( + field: field, + param: node, + ), + ); + } + } + } + + WhereExpressionNode simplify() { + if (nodes.length == 1) { + return nodes.first; + } + bool isSame = true; + WhereExpressionOperator? operator; + for (final node in nodes) { + if (node is WhereExpressionOperatorNode) { + if (operator == null) { + operator = node.operator; + } else if (operator != node.operator) { + isSame = false; + break; + } + } else { + isSame = false; + break; + } + } + if (isSame && operator != null) { + final simplifiedNodes = nodes.fold>( + [], + (previousValue, element) { + previousValue.addAll(element.nodes); + return previousValue; + }, + ); + return WhereExpressionOperatorNode( + operator: operator, + nodes: simplifiedNodes, + ); + } + return this; + } + + Map toMap(); +} + +extension MapParse on List { + List> toMap() { + // turn list into map + return fold>>( + [], + (previousValue, element) { + if (element is WhereExpressionCompareNode) { + previousValue.add({element.field: element.param}); + } else if (element is WhereExpressionOperatorNode) { + previousValue.add({element.operator.name: element.nodes.toMap()}); + } + return previousValue; + }, + ); + } +} + +class WhereExpressionOperatorNode extends WhereExpressionNode { + final WhereExpressionOperator operator; + WhereExpressionOperatorNode({ + required this.operator, + required super.nodes, + }); + + @override + Map>> toMap() { + return {operator.name: nodes.toMap()}; + } +} + +class WhereExpressionCompareNode extends WhereExpressionNode { + final String field; + final WhereParam param; + WhereExpressionCompareNode({ + required this.field, + required this.param, + }); + + @override + Map toMap() { + return {field: param}; + } +} + +enum WhereOperator { + eq, + gt, + gte, + lt, + lte, + between, + notEq, + like, + inList, + notInList, + and, + or, +} + +Map and(List> input) { + return { + dAnd: WhereExpressionOperatorNode( + operator: WhereExpressionOperator.and, + nodes: _getNodesFromMap(input), + ), + }; +} + +Map or(List> input) { + return { + dOr: WhereExpressionOperatorNode( + operator: WhereExpressionOperator.or, + nodes: _getNodesFromMap(input), + ), + }; +} + +List _getNodesFromMap( + List> value, +) { + final List nodes = []; + + for (final item in value) { + for (final entry in item.entries) { + nodes.add( + WhereExpressionCompareNode( + field: entry.key, + param: entry.value, + ), + ); + } + } + + return nodes; +} + +WhereParam inList(T value) { + return WhereParam( + operator: WhereOperator.inList, + value: value, + ); +} + +WhereParam between(T start, T end) { + return WhereParam( + operator: WhereOperator.between, + start: start, + end: end, + ); +} + +WhereParam eq(T value) { + return WhereParam( + operator: WhereOperator.eq, + value: value, + ); +} + +WhereParam gt(T value) { + return WhereParam( + operator: WhereOperator.gt, + value: value, + ); +} + +WhereParam gte(T value) { + return WhereParam( + operator: WhereOperator.gte, + value: value, + ); +} + +WhereParam like(String value) { + return WhereParam( + operator: WhereOperator.like, + value: value, + ); +} + +WhereParam lt(T value) { + return WhereParam( + operator: WhereOperator.lt, + value: value, + ); +} + +WhereParam lte(T value) { + return WhereParam( + operator: WhereOperator.lte, + value: value, + ); +} + +WhereParam notInList(T value) { + return WhereParam( + operator: WhereOperator.notInList, + value: value, + ); +} + +WhereParam notEq(T value) { + return WhereParam( + operator: WhereOperator.notEq, + value: value, + ); +} diff --git a/packages/arcade_orm/lib/src/table.dart b/packages/arcade_orm/lib/src/table.dart new file mode 100644 index 0000000..6cd01df --- /dev/null +++ b/packages/arcade_orm/lib/src/table.dart @@ -0,0 +1,192 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; + +typedef ArcadeOrmTableConverter = ({ + FutureOr Function(Map json) fromJson, + FutureOr> Function() toJson, +}); + +typedef ArcadeOrmTableFromConverter = FutureOr Function( + Map json, +); + +mixin ArcadeOrmTable { + late String name; + final List _tables = []; + + ArcadeOrmTableSchema getTable(String name) { + try { + return _tables.firstWhere((element) => element.tableName == name); + } catch (e) { + throw StateError( + "Table ($name) not found. Please make sure table schema is initialized", + ); + } + } +} + +typedef ArcadeTableIndexRecord = (int direction, {bool? unique}); + +typedef ArcadeOrmRelationshipRecord = ({ + ArcadeOrmRelationshipType type, + String table, + String localKey, + String foreignKey, +}); + +enum ArcadeOrmRelationshipType { + hasOne, + hasMany, + belongsTo, + belongsToMany, +} + +abstract class ArcadeOrmTableSchema { + late final ArcadeOrm $orm; + String get tableName; + + Map get schema; + Map get index => {}; + Map get relations => {}; + + ArcadeOrmTableSchema(ArcadeOrm orm) { + $orm = orm; + orm._tables.add(this); + } + + ArcadeOrmTransaction transaction() { + return $orm.transaction(); + } + + ArcadeOrmTableRawOperator raw({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableRawOperator( + orm: $orm, + operator: TableOperator.raw, + transaction: transaction, + ); + } + + ArcadeOrmTableFindOperator count({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableFindOperator( + orm: $orm, + operator: TableOperator.count, + transaction: transaction, + ); + } + + ArcadeOrmTableFindOperator findOne({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableFindOperator( + orm: $orm, + operator: TableOperator.findOne, + transaction: transaction, + ); + } + + ArcadeOrmTableFindOperator findMany({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableFindOperator( + orm: $orm, + operator: TableOperator.findMany, + transaction: transaction, + ); + } + + ArcadeOrmTableInsertOperator insert({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableInsertOperator( + orm: $orm, + operator: TableOperator.insert, + transaction: transaction, + ); + } + + ArcadeOrmTableUpdateOperator update({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableUpdateOperator( + orm: $orm, + operator: TableOperator.update, + transaction: transaction, + ); + } + + ArcadeOrmTableDeleteOperator delete({ArcadeOrmTransaction? transaction}) { + return ArcadeOrmTableDeleteOperator( + orm: $orm, + operator: TableOperator.delete, + transaction: transaction, + ); + } +} + +abstract class ColumnMeta { + final bool isNullable; + const ColumnMeta({this.isNullable = false}); +} + +class ColumnInt extends ColumnMeta { + const ColumnInt({super.isNullable}); +} + +class ColumnIntRange extends ColumnMeta { + const ColumnIntRange(); +} + +class ColumnFloat extends ColumnMeta { + const ColumnFloat(); +} + +class ColumnBool extends ColumnMeta { + const ColumnBool(); +} + +class ColumnString extends ColumnMeta { + final dynamic search; + + const ColumnString({this.search}); +} + +class ColumnStringEnum extends ColumnMeta { + const ColumnStringEnum(); +} + +class ColumnStringSet extends ColumnMeta { + const ColumnStringSet(); +} + +class ColumnBinary extends ColumnMeta { + const ColumnBinary(); +} + +class ColumnDate extends ColumnMeta { + const ColumnDate(); +} + +class ColumnDateRange extends ColumnMeta { + const ColumnDateRange(); +} + +class ColumnJson extends ColumnMeta { + const ColumnJson(); +} + +class ColumnComposite extends ColumnMeta { + const ColumnComposite(); +} + +class ColumnGeoJson extends ColumnMeta { + const ColumnGeoJson(); +} + +class ColumnObjectId extends ColumnMeta { + const ColumnObjectId(); +} + +class ColumnArray extends ColumnMeta { + const ColumnArray(); +} + +abstract interface class ColumnCustomDomain extends ColumnMeta {} + +class ColumnCustom extends ColumnMeta { + final String raw; + + const ColumnCustom({required this.raw}); +} diff --git a/packages/arcade_orm/pubspec.yaml b/packages/arcade_orm/pubspec.yaml new file mode 100644 index 0000000..2b9930b --- /dev/null +++ b/packages/arcade_orm/pubspec.yaml @@ -0,0 +1,17 @@ +name: arcade_orm +description: A starting point for Dart libraries or applications. +version: 0.0.1 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.0.5 + +# Add regular dependencies here. +dependencies: + meta: ^1.9.1 + # path: ^1.8.0 + +dev_dependencies: + lint: ^2.1.2 + mocktail: ^1.0.1 + test: ^1.21.0 diff --git a/packages/arcade_orm/test/src/adapter_test.dart b/packages/arcade_orm/test/src/adapter_test.dart new file mode 100644 index 0000000..cdd0649 --- /dev/null +++ b/packages/arcade_orm/test/src/adapter_test.dart @@ -0,0 +1,329 @@ +import 'dart:async'; + +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +class Transaction extends ArcadeOrmTransaction { + int commitCount = 0; + int rollbackCount = 0; + int startCount = 0; + + @override + Future $commit() async { + commitCount++; + } + + @override + Future $rollback() async { + rollbackCount++; + } + + @override + Future $startTransaction() async { + startCount++; + } +} + +void main() { + final mockAdapter = MockAdapter(); + group('transaction', () { + setUp(() { + when(() => mockAdapter.transaction()).thenReturn(Transaction()); + }); + + tearDown(clearOrms); + + test("commit flow", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.commit(); + final Transaction(:startCount, :commitCount, :rollbackCount) = trx; + expect(startCount, 1); + expect(commitCount, 1); + expect(rollbackCount, 0); + }); + + test("rollback flow", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.rollback(); + final Transaction(:startCount, :commitCount, :rollbackCount) = trx; + expect(startCount, 1); + expect(commitCount, 0); + expect(rollbackCount, 1); + }); + + test("callback commit flow", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + final data = await trx.start((trx) async { + return {"key": "value"}; + }); + expect(data, {"key": "value"}); + final Transaction(:startCount, :commitCount, :rollbackCount) = trx; + expect(startCount, 1); + expect(commitCount, 1); + expect(rollbackCount, 0); + }); + + test("callback rollback flow", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + try { + await trx.start((trx) async { + throw Exception(); + }); + } catch (_) { + final Transaction(:startCount, :commitCount, :rollbackCount) = trx; + expect(startCount, 1); + expect(commitCount, 0); + expect(rollbackCount, 1); + return; + } + throw Exception("Expected Exception"); + }); + + test("early commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.commit(); + fail("Expected an Exception"); + } catch (e) { + if (e is! ArcadeOrmException) { + fail("Expected ArcadeOrmException"); + } + expect( + e.message, + "Cannot Commit a transaction that has not been started", + ); + } + }); + + test("callback early commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start((trx) async { + await trx.commit(); + }); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Rollback a transaction that is already committed", + ); + return; + } + fail("Expected ArcadeOrmException"); + }); + + test("early rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.rollback(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Rollback a transaction that has not been started", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("callback - early rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start((trx) async { + await trx.rollback(); + }); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Rollback a transaction that has been rolled back", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("late start after commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.commit(); + await trx.start(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Start a transaction that is already committed", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("callback - late start after commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start((trx) async {}); + await trx.start(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Start a transaction that is already committed", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("late start after rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.rollback(); + await trx.start(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Start a transaction that is already rolled back", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("callback - late start after rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + try { + await trx.start((trx) async { + throw Exception(); + }); + } catch (_) {} + await trx.start(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Start a transaction that is already rolled back", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("commit after rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.rollback(); + await trx.commit(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Commit a transaction that has been rolled back", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("callback - commit after rollback", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + try { + await trx.start((trx) async { + throw Exception(); + }); + } catch (_) {} + await trx.commit(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Commit a transaction that has been rolled back", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("rollback after commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start(); + await trx.commit(); + await trx.rollback(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Rollback a transaction that is already committed", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + + test("callback - rollback after commit", () async { + try { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final trx = arcadeOrm.transaction() as Transaction; + await trx.start((trx) async {}); + await trx.rollback(); + } on ArcadeOrmException catch (e) { + expect( + e.message, + "Cannot Rollback a transaction that is already committed", + ); + return; + } + throw Exception("Expected ArcadeOrmException"); + }); + }); +} diff --git a/packages/arcade_orm/test/src/core_test.dart b/packages/arcade_orm/test/src/core_test.dart new file mode 100644 index 0000000..cb923b5 --- /dev/null +++ b/packages/arcade_orm/test/src/core_test.dart @@ -0,0 +1,38 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +void main() { + group('getArcadeOrmInstance', () { + tearDown(clearOrms); + + test('should throw error if ArcadeOrm is not initialized', () { + expect(() => getArcadeOrmInstance(), throwsStateError); + }); + + test('should return an instance if init() was called', () async { + final arcadeOrm = await ArcadeOrm.init(adapter: MockAdapter()); + expect(getArcadeOrmInstance(), arcadeOrm); + }); + + test('should return the correct instance if name is provided', () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: MockAdapter(), + name: 'test', + ); + expect(getArcadeOrmInstance('test'), arcadeOrm); + }); + + test('should return correct instance if multiple ORMs exist', () async { + final defaultArcadeOrm = await ArcadeOrm.init(adapter: MockAdapter()); + final namedArcadeOrm = await ArcadeOrm.init( + adapter: MockAdapter(), + name: 'test', + ); + expect(getArcadeOrmInstance(), defaultArcadeOrm); + expect(getArcadeOrmInstance('test'), namedArcadeOrm); + }); + }); +} diff --git a/packages/arcade_orm/test/src/query/delete_test.dart b/packages/arcade_orm/test/src/query/delete_test.dart new file mode 100644 index 0000000..13b0b46 --- /dev/null +++ b/packages/arcade_orm/test/src/query/delete_test.dart @@ -0,0 +1,230 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/include.dart'; +import 'package:arcade_orm/src/query/where.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +class MockTransaction extends Mock implements ArcadeOrmTransaction {} + +class UserTable extends ArcadeOrmTableSchema { + UserTable(super.orm); + + @override + final String tableName = "user"; + + static const String id = "id"; + + @override + Map schema = {id: const ColumnInt()}; + + @override + Map index = {}; + + @override + Map relations = {}; +} + +class RoleTable extends ArcadeOrmTableSchema { + RoleTable(super.orm); + + @override + final String tableName = "role"; + + static const String name = "name"; + static const String foo = "foo"; + + @override + Map schema = { + name: const ColumnString(), + foo: const ColumnString(), + }; +} + +void main() { + final mockAdapter = MockAdapter(); + final mockTransaction = MockTransaction(); + + group('delete', () { + setUp(() { + when(() => mockAdapter.transaction()).thenReturn(mockTransaction); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + group("success", () { + setUp(() { + when( + () => mockAdapter.operate( + operator: TableOperator.delete, + transaction: null, + isVerbose: false, + whereParams: any(named: "whereParams"), + ), + ).thenAnswer( + (_) => Future.value({"deleted": 1}), + ); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + test("operate where", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final userTable = UserTable(arcadeOrm); + final roleTable = RoleTable(arcadeOrm); + + final deleteQuery = userTable.delete() + ..include(roleTable, where: {"name": notEq("admin")}) + ..where({"id": eq(1)}); + + await deleteQuery.exec(); + + final captured = verify( + () => mockAdapter.operate( + operator: TableOperator.delete, + transaction: null, + isVerbose: false, + whereParams: captureAny(named: "whereParams"), + // havingParams: [], + selectParams: [], + includeParams: any(named: "includeParams"), + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + ), + ).captured; + final capturedWhereParams = + (captured.first as WhereExpressionNode).toMap(); + expect( + capturedWhereParams["id"], + equals(WhereParam(operator: WhereOperator.eq, value: 1)), + ); + }); + + test("operate include", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final userTable = UserTable(arcadeOrm); + final roleTable = RoleTable(arcadeOrm); + + final deleteQuery = userTable.delete() + ..include(roleTable, where: {"name": notEq("admin")}) + ..where({"id": eq(1)}); + + await deleteQuery.exec(); + + final captured = verify( + () => mockAdapter.operate( + operator: TableOperator.delete, + transaction: null, + isVerbose: false, + whereParams: any(named: "whereParams"), + // havingParams: [], + selectParams: [], + includeParams: captureAny(named: "includeParams"), + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + ), + ).captured; + final capturedIncludeParams = + (captured.first as List).first; + expect(capturedIncludeParams.tableName, equals("role")); + expect( + capturedIncludeParams.where?.toMap()["name"], + equals( + WhereParam( + operator: WhereOperator.notEq, + value: "admin", + ), + ), + ); + expect(capturedIncludeParams.as, equals(null)); + expect(capturedIncludeParams.on, equals(null)); + expect(capturedIncludeParams.joinType, equals(JoinOperation.inner)); + }); + + test("data", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final deleteQuery = table.delete()..where({"id": eq(1)}); + final data = await deleteQuery.exec(); + expect(data, isA()); + expect((data as ExecResultData).data, equals({"deleted": 1})); + }); + + test("operate with transaction", () async { + when(() => mockTransaction.start()) + .thenAnswer((_) async => Future.value()); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final trx = table.transaction(); + await trx.start(); + final deleteQuery = table.delete(transaction: trx) + ..where({"id": eq(1)}); + await deleteQuery.exec(); + final captured = verify( + () => mockAdapter.operate( + operator: TableOperator.delete, + transaction: trx, + isVerbose: false, + whereParams: captureAny(named: "whereParams"), + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + ), + ).captured; + + final capturedWhereParams = + (captured.first as WhereExpressionNode).toMap(); + expect( + capturedWhereParams["id"], + equals(WhereParam(operator: WhereOperator.eq, value: 1)), + ); + }); + }); + + test("db record - failure", () async { + when( + () => mockAdapter.operate( + operator: TableOperator.delete, + transaction: null, + isVerbose: false, + whereParams: any(named: "whereParams"), + ), + ).thenThrow( + ArcadeOrmException(message: "Delete Failed", originalError: null), + ); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final deleteQuery = table.delete()..where({"id": eq(1)}); + final data = await deleteQuery.exec(); + expect(data, isA()); + expect((data as ExecResultFailure).exception.message, "Delete Failed"); + }); + }); +} diff --git a/packages/arcade_orm/test/src/query/insert_test.dart b/packages/arcade_orm/test/src/query/insert_test.dart new file mode 100644 index 0000000..67af12c --- /dev/null +++ b/packages/arcade_orm/test/src/query/insert_test.dart @@ -0,0 +1,158 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +class MockTransaction extends Mock implements ArcadeOrmTransaction {} + +class UserTable extends ArcadeOrmTableSchema { + UserTable(super.orm); + + @override + final String tableName = "user"; + + static const String id = "id"; + static const String name = "name"; + static const String email = "email"; + static const String age = "age"; + + @override + Map schema = { + id: const ColumnInt(), + name: const ColumnString(), + email: const ColumnString(), + age: const ColumnInt(), + }; +} + +void main() { + final mockAdapter = MockAdapter(); + final mockTransaction = MockTransaction(); + group('insert', () { + setUp(() { + when(() => mockAdapter.transaction()).thenReturn(mockTransaction); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + group("success", () { + setUp(() { + when( + () => mockAdapter.operate( + operator: TableOperator.insert, + transaction: null, + isVerbose: false, + insertWithParams: any(named: "insertWithParams"), + ), + ).thenAnswer( + (_) => Future.value({"nInserted": 1}), + ); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + test("operate", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final insertQuery = table.insert() + ..insertWith({"name": "foo", "age": 20}) + ..insertWith({"email": "foo@examle.com"}); + await insertQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.insert, + transaction: null, + isVerbose: false, + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [ + {"name": "foo", "age": 20}, + {"email": "foo@examle.com"}, + ], + ), + ).called(1); + }); + + test("data", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final insertQuery = table.insert() + ..insertWith({"name": "foo", "age": 20}) + ..insertWith({"email": "foo@examle.com"}); + final data = await insertQuery.exec(); + expect(data, isA()); + expect((data as ExecResultData).data, equals({"nInserted": 1})); + }); + + test("operate with transaction", () async { + when(() => mockTransaction.start()) + .thenAnswer((_) async => Future.value()); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final trx = table.transaction(); + await trx.start(); + final insertQuery = table.insert(transaction: trx) + ..insertWith({"name": "foo", "age": 20}); + await insertQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.insert, + transaction: trx, + isVerbose: false, + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [ + {"name": "foo", "age": 20}, + ], + ), + ).called(1); + }); + }); + + test("db record - failure", () async { + when( + () => mockAdapter.operate( + operator: TableOperator.insert, + transaction: null, + isVerbose: false, + insertWithParams: any(named: "insertWithParams"), + ), + ).thenThrow( + ArcadeOrmException(message: "Insert Failed", originalError: null), + ); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final insertQuery = table.insert() + ..insertWith({"name": "foo", "age": 20}) + ..insertWith({"email": "foo@examle.com"}); + final data = await insertQuery.exec(); + expect(data, isA()); + expect((data as ExecResultFailure).exception.message, "Insert Failed"); + }); + }); +} diff --git a/packages/arcade_orm/test/src/query/raw_test.dart b/packages/arcade_orm/test/src/query/raw_test.dart new file mode 100644 index 0000000..ea19bb4 --- /dev/null +++ b/packages/arcade_orm/test/src/query/raw_test.dart @@ -0,0 +1,197 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +class MockTransaction extends Mock implements ArcadeOrmTransaction {} + +class UserTable extends ArcadeOrmTableSchema { + UserTable(super.orm); + + @override + final String tableName = "user"; + + @override + Map schema = {}; +} + +void main() { + final mockAdapter = MockAdapter(); + final mockTransaction = MockTransaction(); + group('raw', () { + setUp(() { + when(() => mockAdapter.transaction()).thenReturn(mockTransaction); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + group("success", () { + setUp(() { + when( + () => mockAdapter.operate( + operator: TableOperator.raw, + transaction: null, + isVerbose: false, + rawSql: any(named: "rawSql"), + rawNoSqlAggregate: any(named: "rawNoSqlAggregate"), + rawNoSqlAggregateOptions: any(named: "rawNoSqlAggregateOptions"), + ), + ).thenAnswer( + (_) => Future.value({"nResult": 1}), + ); + }); + + test("operate sql", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final rawQuery = table.raw() + ..sql( + "INSERT INTO user (name, email) VALUES (@name, @email)", + params: { + "name:": "John", + "email": "john@example.com", + }, + ); + await rawQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.raw, + transaction: null, + isVerbose: false, + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + rawSql: "INSERT INTO user (name, email) VALUES (@name, @email)", + rawParams: { + "name:": "John", + "email": "john@example.com", + }, + ), + ).called(1); + }); + test("operate noSql", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final rawQuery = table.raw() + ..aggregate( + [ + { + "\$match": { + "name:": "John", + "email": "john@example.com", + }, + }, + { + "\$group": { + "_id": "\$name", + "count": {"\$sum"}, + }, + }, + ], + options: { + "useDisk": "true", + }, + ); + await rawQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.raw, + transaction: null, + isVerbose: false, + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + rawNoSqlAggregate: [ + { + "\$match": { + "name:": "John", + "email": "john@example.com", + }, + }, + { + "\$group": { + "_id": "\$name", + "count": {"\$sum"}, + }, + }, + ], + rawNoSqlAggregateOptions: { + "useDisk": "true", + }, + ), + ).called(1); + }); + test('with transaction', () async { + when(() => mockTransaction.start()) + .thenAnswer((_) async => Future.value()); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final trx = table.transaction(); + await trx.start(); + final rawQuery = table.raw(transaction: trx) + ..sql("SELECT * FROM user WHERE id = @id", params: {"id": 1}); + await rawQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.raw, + transaction: trx, + isVerbose: false, + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {}, + insertWithParams: [], + rawSql: "SELECT * FROM user WHERE id = @id", + rawParams: {"id": 1}, + ), + ).called(1); + }); + }); + + test("db record - failure", () async { + when( + () => mockAdapter.operate( + operator: TableOperator.raw, + transaction: null, + isVerbose: false, + rawParams: any(named: "rawParams"), + rawSql: any(named: "rawSql"), + rawNoSqlAggregate: any(named: "rawNoSqlAggregate"), + rawNoSqlAggregateOptions: any(named: "rawNoSqlAggregateOptions"), + ), + ).thenThrow( + ArcadeOrmException(message: "Operation Failed", originalError: null), + ); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final rawQuery = table.raw() + ..sql("SELECT * FROM user WHERE id = @id", params: {"id": 1}); + final data = await rawQuery.exec(); + expect(data, isA()); + expect((data as ExecResultFailure).exception.message, "Operation Failed"); + }); + }); +} diff --git a/packages/arcade_orm/test/src/query/update_test.dart b/packages/arcade_orm/test/src/query/update_test.dart new file mode 100644 index 0000000..1b770d6 --- /dev/null +++ b/packages/arcade_orm/test/src/query/update_test.dart @@ -0,0 +1,212 @@ +import 'package:arcade_orm/arcade_orm.dart'; +import 'package:arcade_orm/src/query/where.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAdapter extends Mock implements ArcadeOrmAdapterBase {} + +class MockTransaction extends Mock implements ArcadeOrmTransaction {} + +class UserTable extends ArcadeOrmTableSchema { + UserTable(super.orm); + + @override + final String tableName = "user"; + + static const String id = "id"; + static const String name = "name"; + static const String email = "email"; + static const String age = "age"; + + @override + Map schema = { + id: const ColumnInt(), + name: const ColumnString(), + email: const ColumnString(), + age: const ColumnInt(), + }; +} + +void main() { + final mockAdapter = MockAdapter(); + final mockTransaction = MockTransaction(); + + group('update', () { + setUp(() { + when(() => mockAdapter.transaction()).thenReturn(mockTransaction); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + group("success", () { + setUp(() { + when( + () => mockAdapter.operate( + operator: TableOperator.update, + transaction: null, + isVerbose: false, + whereParams: any(named: "whereParams"), + updateWithParams: any(named: "updateWithParams"), + ), + ).thenAnswer( + (_) => Future.value({"nUpdated": 1}), + ); + }); + + tearDown(() { + clearOrms(); + reset(mockAdapter); + reset(mockTransaction); + }); + + test("operate where", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final updateQuery = table.update() + ..where( + and([ + { + UserTable.name: eq("bar"), + UserTable.age: gt(20), + } + ]), + ) + ..updateWith({ + UserTable.name: "foo", + UserTable.age: 20, + }); + + await updateQuery.exec(); + + final captured = verify( + () => mockAdapter.operate( + operator: TableOperator.update, + transaction: null, + isVerbose: false, + whereParams: captureAny(named: "whereParams"), + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: any(named: "updateWithParams"), + insertWithParams: [], + ), + ).captured; + + final capturedWhereParams = + (captured.first as WhereExpressionNode).toMap(); + + final andExpr = capturedWhereParams[WhereExpressionOperator.and.name] + as List>; + + expect( + andExpr.first[UserTable.name], + equals(WhereParam(operator: WhereOperator.eq, value: "bar")), + ); + expect( + andExpr.last[UserTable.age], + equals(WhereParam(operator: WhereOperator.gt, value: 20)), + ); + }); + + test("operate updateWith", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final updateQuery = table.update() + ..where( + and([ + { + UserTable.name: eq("bar"), + UserTable.age: gt(20), + } + ]), + ) + ..updateWith({ + UserTable.name: "foo", + UserTable.age: 20, + }) + ..updateWith({ + UserTable.age: 30, + UserTable.email: "foo@example.com", + }); + + await updateQuery.exec(); + + final captured = verify( + () => mockAdapter.operate( + operator: TableOperator.update, + transaction: null, + isVerbose: false, + whereParams: any(named: "whereParams"), + // havingParams: [], + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: captureAny(named: "updateWithParams"), + insertWithParams: [], + ), + ).captured; + + final updateWithParams = captured.first as Map; + + expect( + updateWithParams, + equals({ + UserTable.name: "foo", + UserTable.age: 30, + UserTable.email: "foo@example.com", + }), + ); + }); + + test("data", () async { + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final udpateQuery = table.update() + ..updateWith({"name": "foo", "age": 20}); + final data = await udpateQuery.exec(); + expect(data, isA()); + expect((data as ExecResultData).data, equals({"nUpdated": 1})); + }); + + test("operate with transaction", () async { + when(() => mockTransaction.start()) + .thenAnswer((_) async => Future.value()); + final arcadeOrm = await ArcadeOrm.init( + adapter: mockAdapter, + ); + final table = UserTable(arcadeOrm); + final trx = table.transaction(); + await trx.start(); + final insertQuery = table.update(transaction: trx) + ..updateWith({"name": "foo", "age": 20}); + await insertQuery.exec(); + verify( + () => mockAdapter.operate( + operator: TableOperator.update, + transaction: trx, + isVerbose: false, + whereParams: any(named: "whereParams"), + selectParams: [], + includeParams: [], + groupParams: [], + sortParams: [], + updateWithParams: {"name": "foo", "age": 20}, + ), + ).called(1); + }); + }); + }); +} diff --git a/samples/todo_api/package.json b/samples/todo_api/package.json index f334c71..3ecec89 100644 --- a/samples/todo_api/package.json +++ b/samples/todo_api/package.json @@ -2,10 +2,10 @@ "name": "todo_api", "version": "1.0.0", "description": "A TODO API implemented with Dartseid and Prisma", - "homepage": "https://github.com/exaby73/dartsied/tree/main", + "homepage": "https://github.com/dartarcade/arcade/tree/main", "repository": { "type": "git", - "url": "git+https://github.com/exaby73/dartsied.git#main" + "url": "git+https://github.com/dartarcade/arcade.git#main" }, "author": "exaby73", "license": "MIT",