From cc00d7d630051580c89564d7c4b52154064ec42e Mon Sep 17 00:00:00 2001 From: gmpassos Date: Sun, 3 Oct 2021 16:12:33 -0300 Subject: [PATCH] v1.0.13 - Added `TypeInfo` to represent better types with generics. - Added `TableRelationshipReference` for use in `TableScheme`. - Added `TimedMap` to help with timed caches. - Added `KeyConditionIN` and `KeyConditionNotIN`. - Entities: - Added support to relationship fields. - Added support for List fields pointing to another entity. - `SQLEntityRepository`: - Added support to UPDATE. - Added support to relationship tables. --- CHANGELOG.md | 13 + lib/src/bones_api_base.dart | 2 +- lib/src/bones_api_entity.dart | 488 ++++++++++++---- lib/src/bones_api_entity_adapter.dart | 548 +++++++++++++++++- lib/src/bones_api_entity_adapter_memory.dart | 55 +- lib/src/bones_api_entity_adapter_postgre.dart | 157 ++++- lib/src/bones_api_entity_sql.dart | 341 ++++++++--- lib/src/bones_api_utils.dart | 7 +- pubspec.yaml | 4 +- test/bones_api_entity_test.dart | 100 +++- test/bones_api_postgre_test.dart | 91 ++- test/bones_api_test.reflection.g.dart | 10 +- test/bones_api_test_entities.dart | 170 +++++- .../bones_api_test_entities.reflection.g.dart | 355 +++++++++++- test/bones_api_utils_test.dart | 56 ++ 15 files changed, 2030 insertions(+), 367 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a551cf3..794a54d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.0.13 + +- Added `TypeInfo` to represent better types with generics. +- Added `TableRelationshipReference` for use in `TableScheme`. +- Added `TimedMap` to help with timed caches. +- Added `KeyConditionIN` and `KeyConditionNotIN`. +- Entities: + - Added support to relationship fields. + - Added support for List fields pointing to another entity. +- `SQLEntityRepository`: + - Added support to UPDATE. + - Added support to relationship tables. + ## 1.0.12 - CLI: diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 87f53fc..71a2376 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -11,7 +11,7 @@ import 'bones_api_extension.dart'; /// Root class of an API. abstract class APIRoot { // ignore: constant_identifier_names - static const String VERSION = '1.0.12'; + static const String VERSION = '1.0.13'; static final Map _instances = {}; diff --git a/lib/src/bones_api_entity.dart b/lib/src/bones_api_entity.dart index 767ad10..ff569ae 100644 --- a/lib/src/bones_api_entity.dart +++ b/lib/src/bones_api_entity.dart @@ -23,7 +23,7 @@ abstract class Entity { V? getField(String key); - Type? getFieldType(String key); + TypeInfo? getFieldType(String key); void setField(String key, V? value); @@ -96,6 +96,11 @@ abstract class EntityHandler with FieldsFromMap { throw StateError('Invalid EntityHandler type: $type ?? $O'); } + if (O != type) { + throw StateError( + 'EntityHandler generic type `O` should be the same of parameter `type`: O:$O != type:$type'); + } + this.provider._register(this); _jsonReviver = _defaultJsonReviver; @@ -106,14 +111,8 @@ abstract class EntityHandler with FieldsFromMap { return type != Object && type != dynamic && !isPrimitiveType(type); } - static bool isPrimitiveType([Type? type]) { - type ??= T; - return type == String || - type == int || - type == double || - type == num || - type == bool; - } + static bool isPrimitiveType([Type? type]) => + TypeParser.isPrimitiveType(type); EntityHandler? getEntityHandler({T? obj, Type? type}) { if (T == O && isValidType()) { @@ -135,7 +134,27 @@ abstract class EntityHandler with FieldsFromMap { List fieldsNames([O? o]); - Map fieldsTypes([O? o]); + Map fieldsTypes([O? o]); + + Map? _fieldsWithTypeList; + + Map fieldsWithTypeList([O? o]) => _fieldsWithTypeList ??= + fieldsWithType((_, fieldType) => fieldType.isList, o); + + Map? _fieldsWithTypeListEntity; + + Map fieldsWithTypeListEntity([O? o]) => + _fieldsWithTypeListEntity ??= + fieldsWithType((_, fieldType) => fieldType.isListEntity, o); + + Map fieldsWithType( + bool Function(String fieldName, TypeInfo fieldType) typeFilter, + [O? o]) { + return Map.unmodifiable( + Map.fromEntries(fieldsTypes(o).entries.where((e) { + return typeFilter(e.key, e.value); + }))); + } void inspectObject(O? o); @@ -154,46 +173,93 @@ abstract class EntityHandler with FieldsFromMap { } FutureOr resolveFieldValue( - String fieldName, Type? fieldType, Object? value) { + String fieldName, TypeInfo? fieldType, Object? value) { if (value == null) return null; - switch (fieldType) { - case String: - return TypeParser.parseString(value); - case int: - return TypeParser.parseInt(value); - case double: - return TypeParser.parseDouble(value); - case num: - return TypeParser.parseNum(value); - case DateTime: - return TypeParser.parseDateTime(value); - case List: - case Iterable: - return TypeParser.parseList(value); - case Set: - return TypeParser.parseSet(value); - case Map: - return TypeParser.parseMap(value); - case MapEntry: - return TypeParser.parseMapEntry(value); - default: - { - if (value is num || - value is String || - value is Map) { - var o = EntityRepository.resolveEntityFromMap( - entityMap: value, - entityType: fieldType, - entityHandlerProvider: provider); - return o ?? value; - } - - return value; + var resolved = fieldType?.parse(value); + if (resolved != null) { + return resolved; + } + + if (value is num || value is String || value is Map) { + var o = EntityRepository.resolveEntityFromMap( + entityMap: value, + entityType: fieldType?.type, + entityHandlerProvider: provider); + return o ?? value; + } + + return value; + } + + FutureOr resolveEntityFieldValue(O o, String key, dynamic value) { + var fieldType = getFieldType(o, key); + return resolveValueByType(fieldType, value); + } + + T? resolveValueByType(TypeInfo? type, Object? value) { + if (type == null) { + return value as T?; + } + + if (value is Map) { + if (type.type == Map) { + return type.parse(value); + } else { + var valEntityHandler = _resolveEntityHandler(type); + var resolved = valEntityHandler != null + ? valEntityHandler.createFromMap(value) + : value; + return resolved as T?; + } + } else if (value is List) { + if (type.type == List && type.hasArguments) { + var elementType = type.arguments.first; + var valEntityHandler = _resolveEntityHandler(elementType); + + if (valEntityHandler != null) { + var list = TypeParser.parseList(value, + elementParser: (e) => + valEntityHandler.resolveValueByType(elementType, e)); + + if (list == null) return null; + return valEntityHandler.castList(list, elementType.type)! as T; + } else { + var list = TypeParser.parseList(value, + elementParser: (e) => resolveValueByType(elementType, e)); + + if (list == null) return null; + return list as T; } + } else { + return type.parse(value); + } + } else { + return type.parse(value); } } + List? castList(List list, Type type) { + if (type == this.type) { + return List.from(list); + } + return null; + } + + Iterable? castIterable(Iterable itr, Type type) { + if (type == this.type) { + return itr.cast(); + } + return null; + } + + EntityHandler? _resolveEntityHandler(TypeInfo fieldType) { + var valEntityHandler = getEntityHandler(type: fieldType.type); + valEntityHandler ??= + getEntityRepository(type: fieldType.type)?.entityHandler; + return valEntityHandler; + } + Map? _fieldsNamesIndexes; Map fieldsNamesIndexes([O? o]) { @@ -224,7 +290,7 @@ abstract class EntityHandler with FieldsFromMap { return _fieldsNamesSimple!; } - Type? getFieldType(O? o, String key); + TypeInfo? getFieldType(O? o, String key); V? getField(O o, String key); @@ -305,45 +371,11 @@ abstract class EntityHandler with FieldsFromMap { } FutureOr setFieldValueDynamic(O o, String key, dynamic value) { - if (value == null) { - return null; - } - - var fieldType = getFieldType(o, key); - - if (fieldType == null || - fieldType == value.runtimeType || - isPrimitiveType(fieldType)) { - setField(o, key, value); - return value; - } else if (value is Map && fieldType == Map) { - setField(o, key, value); - return value; - } else if (value is Map) { - var valEntityHandler = getEntityHandler(type: fieldType); - valEntityHandler ??= getEntityRepository(type: fieldType)?.entityHandler; - - if (valEntityHandler != null) { - var valEntity = valEntityHandler.createFromMap(value); - setField(o, key, valEntity); - return valEntity; - } else { - setField(o, key, value); - return value; - } - } else { - var valRepo = getEntityRepository(type: fieldType); - var retValDynamic = valRepo?.selectByID(value); - - return retValDynamic.resolveMapped((valDynamic) { - valDynamic ??= value; - var retValResolved = resolveFieldValue(key, fieldType, valDynamic); - return retValResolved.resolveMapped((valResolved) { - setField(o, key, valResolved); - return valResolved; - }); - }); - } + var retValue2 = resolveEntityFieldValue(o, key, value); + return retValue2.resolveMapped((value2) { + setField(o, key, value2); + return value2; + }); } final Set _knownEntityRepositoryProviders = @@ -403,7 +435,8 @@ class GenericEntityHandler extends EntityHandler { var idFieldsName = _idFieldsName; if (idFieldsName == null && o != null) { - idFieldsName = _idFieldsName = o.idFieldName; + inspectObject(o); + idFieldsName = _idFieldsName; } if (idFieldsName == null) { @@ -433,10 +466,10 @@ class GenericEntityHandler extends EntityHandler { return fieldsNames; } - Map? _fieldsTypes; + Map? _fieldsTypes; @override - Map fieldsTypes([O? o]) { + Map fieldsTypes([O? o]) { var fieldsTypes = _fieldsTypes; if (fieldsTypes == null && o != null) { @@ -454,11 +487,11 @@ class GenericEntityHandler extends EntityHandler { @override void inspectObject(O? o) { - if (o != null) { - _idFieldsName ??= o.idFieldName; + if (o != null && _idFieldsName == null) { + _idFieldsName = o.idFieldName; _fieldsNames ??= List.unmodifiable(o.fieldsNames); - _fieldsTypes ??= Map.unmodifiable( - Map.fromEntries( + _fieldsTypes ??= Map.unmodifiable( + Map.fromEntries( _fieldsNames!.map((f) => MapEntry(f, o.getFieldType(f)!)))); } } @@ -502,7 +535,7 @@ class GenericEntityHandler extends EntityHandler { } @override - Type? getFieldType(O? o, String key) { + TypeInfo? getFieldType(O? o, String key) { inspectObject(o); return o?.getFieldType(key); } @@ -533,6 +566,11 @@ class GenericEntityHandler extends EntityHandler { return oRet.resolveMapped((o) => setFieldsFromMap(o, fields)); } } + + @override + String toString() { + return 'GenericEntityHandler{$type}'; + } } class ClassReflectionEntityHandler extends EntityHandler { @@ -558,9 +596,9 @@ class ClassReflectionEntityHandler extends EntityHandler { V? getField(O o, String key) => reflection.getField(key, o); @override - Type? getFieldType(O? o, String key) { + TypeInfo? getFieldType(O? o, String key) { var field = reflection.field(key, o); - return field?.type.type; + return field != null ? TypeInfo.from(field) : null; } @override @@ -654,11 +692,11 @@ class ClassReflectionEntityHandler extends EntityHandler { @override List fieldsNames([O? o]) => _fieldsNames ??= reflection.fieldsNames; - Map? _fieldsTypes; + Map? _fieldsTypes; @override - Map fieldsTypes([O? o]) => _fieldsTypes ??= - Map.unmodifiable(Map.fromEntries( + Map fieldsTypes([O? o]) => _fieldsTypes ??= + Map.unmodifiable(Map.fromEntries( _fieldsNames!.map((f) => MapEntry(f, getFieldType(o, f)!)))); @override @@ -666,6 +704,11 @@ class ClassReflectionEntityHandler extends EntityHandler { var o = reflection.createInstance()!; return setFieldsFromMap(o, fields); } + + @override + String toString() { + return 'ClassReflectionEntityHandler{$classType}'; + } } mixin EntityFieldAccessor { @@ -674,6 +717,8 @@ mixin EntityFieldAccessor { return o.getID(); } else if (entityHandler != null) { return entityHandler.getID(o); + } else if (o is Map) { + return o['id']; } else { throw StateError('getID: No EntityHandler provided for: $o'); } @@ -684,6 +729,8 @@ mixin EntityFieldAccessor { return o.setID(id); } else if (entityHandler != null) { return entityHandler.setID(o, id); + } else if (o is Map) { + o['id'] = id; } else { throw StateError('setID: No EntityHandler provided for: $o'); } @@ -694,6 +741,8 @@ mixin EntityFieldAccessor { return o.getField(key); } else if (entityHandler != null) { return entityHandler.getField(o, key); + } else if (o is Map) { + return o[key]; } else { throw StateError('getField($key): No EntityHandler provided for: $o'); } @@ -705,6 +754,8 @@ mixin EntityFieldAccessor { o.setField(key, value); } else if (entityHandler != null) { entityHandler.setField(o, key, value); + } else if (o is Map) { + o[key] = value; } else { throw StateError('setField($key): No EntityHandler provided for: $o'); } @@ -728,6 +779,12 @@ abstract class EntitySource extends EntityAccessor { }); } + FutureOr> selectByIDs(List ids, + {Transaction? transaction}) { + var ret = ids.map((id) => selectByID(id, transaction: transaction)); + return ret.resolveAll(); + } + FutureOr length({Transaction? transaction}) => count(transaction: transaction); @@ -778,6 +835,9 @@ abstract class EntitySource extends EntityAccessor { Transaction? transaction, int? limit}); + FutureOr> selectRelationship(O? o, String field, + {Object? oId, TypeInfo? fieldType, Transaction? transaction}); + FutureOr> deleteByQuery(String query, {Object? parameters, List? positionalParameters, @@ -802,9 +862,14 @@ abstract class EntitySource extends EntityAccessor { abstract class EntityStorage extends EntityAccessor { EntityStorage(String name) : super(name); + bool isStored(O o, {Transaction? transaction}); + FutureOr store(O o, {Transaction? transaction}); FutureOr storeAll(Iterable o, {Transaction? transaction}); + + FutureOr setRelationship(O o, String field, List values, + {TypeInfo? fieldType, Transaction? transaction}); } class EntityRepositoryProvider with Closable { @@ -1066,6 +1131,13 @@ abstract class EntityRepository extends EntityAccessor }); } + @override + FutureOr> selectByIDs(List ids, + {Transaction? transaction}) { + var ret = ids.map((id) => selectByID(id, transaction: transaction)); + return ret.resolveAll(); + } + FutureOr ensureStored(O o, {Transaction? transaction}); FutureOr ensureReferencesStored(O o, {Transaction? transaction}); @@ -1250,11 +1322,10 @@ class Transaction { op.id = _operations.length; } - FutureOr finishOperation(TransactionOperation op, R result, - bool transactionRoot, bool externalTransaction) { + FutureOr finishOperation(TransactionOperation op, R result) { _markOperationExecuted(op, result); - if (transactionRoot && !externalTransaction && length > 1) { + if (op.transactionRoot && !op.externalTransaction && length > 1) { return resultFuture.then((_) => result); } else { return result; @@ -1406,18 +1477,34 @@ abstract class TransactionOperation { int? id; - TransactionOperation(this.type); + late final Transaction transaction; + late final bool externalTransaction; + late final bool transactionRoot; + + TransactionOperation(this.type, Transaction? transaction) { + externalTransaction = transaction != null; + + var resolvedTransaction = transaction ?? Transaction.executingOrNew(); + this.transaction = resolvedTransaction; + + transactionRoot = + resolvedTransaction.isEmpty && !resolvedTransaction.isExecuting; + + resolvedTransaction.addOperation(this); + } TransactionExecution? execution; String get _commandToString => command == null ? '' : ', command: $command'; + + FutureOr finish(R result) => transaction.finishOperation(this, result); } class TransactionOperationSelect extends TransactionOperation { final EntityMatcher matcher; - TransactionOperationSelect(this.matcher) - : super(TransactionOperationType.select); + TransactionOperationSelect(this.matcher, [Transaction? transaction]) + : super(TransactionOperationType.select, transaction); @override String toString() { @@ -1428,8 +1515,8 @@ class TransactionOperationSelect extends TransactionOperation { class TransactionOperationCount extends TransactionOperation { final EntityMatcher? matcher; - TransactionOperationCount([this.matcher]) - : super(TransactionOperationType.count); + TransactionOperationCount([this.matcher, Transaction? transaction]) + : super(TransactionOperationType.count, transaction); @override String toString() { @@ -1440,8 +1527,8 @@ class TransactionOperationCount extends TransactionOperation { class TransactionOperationStore extends TransactionOperation { final O entity; - TransactionOperationStore(this.entity) - : super(TransactionOperationType.store); + TransactionOperationStore(this.entity, [Transaction? transaction]) + : super(TransactionOperationType.store, transaction); @override String toString() { @@ -1449,11 +1536,65 @@ class TransactionOperationStore extends TransactionOperation { } } +class TransactionOperationUpdate extends TransactionOperation { + final O entity; + + TransactionOperationUpdate(this.entity, [Transaction? transaction]) + : super(TransactionOperationType.update, transaction); + + @override + String toString() { + return 'TransactionOperation[#$id:update]{entity: $entity$_commandToString}'; + } +} + +class TransactionOperationStoreRelationship extends TransactionOperation { + final O entity; + final List others; + + TransactionOperationStoreRelationship(this.entity, this.others, + [Transaction? transaction]) + : super(TransactionOperationType.storeRelationship, transaction); + + @override + String toString() { + return 'TransactionOperation[#$id:storeRelationship]{entity: $entity, other: $others$_commandToString}'; + } +} + +class TransactionOperationConstrainRelationship + extends TransactionOperation { + final O entity; + final List others; + + TransactionOperationConstrainRelationship(this.entity, this.others, + [Transaction? transaction]) + : super(TransactionOperationType.constrainRelationship, transaction); + + @override + String toString() { + return 'TransactionOperation[#$id:constrainRelationship]{entity: $entity, other: $others$_commandToString}'; + } +} + +class TransactionOperationSelectRelationship extends TransactionOperation { + final O entity; + + TransactionOperationSelectRelationship(this.entity, + [Transaction? transaction]) + : super(TransactionOperationType.selectRelationship, transaction); + + @override + String toString() { + return 'TransactionOperation[#$id:selectRelationship]{entity: $entity$_commandToString}'; + } +} + class TransactionOperationDelete extends TransactionOperation { final EntityMatcher matcher; - TransactionOperationDelete(this.matcher) - : super(TransactionOperationType.delete); + TransactionOperationDelete(this.matcher, [Transaction? transaction]) + : super(TransactionOperationType.delete, transaction); @override String toString() { @@ -1461,7 +1602,16 @@ class TransactionOperationDelete extends TransactionOperation { } } -enum TransactionOperationType { select, count, store, update, delete } +enum TransactionOperationType { + select, + count, + store, + storeRelationship, + constrainRelationship, + selectRelationship, + update, + delete +} extension TransactionOperationTypeExtension on TransactionOperationType { String get name { @@ -1472,6 +1622,12 @@ extension TransactionOperationTypeExtension on TransactionOperationType { return 'count'; case TransactionOperationType.store: return 'store'; + case TransactionOperationType.storeRelationship: + return 'storeRelationship'; + case TransactionOperationType.constrainRelationship: + return 'constrainRelationship'; + case TransactionOperationType.selectRelationship: + return 'selectRelationship'; case TransactionOperationType.update: return 'update'; case TransactionOperationType.delete: @@ -1546,25 +1702,30 @@ abstract class IterableEntityRepository extends EntityRepository }); } + @override + bool isStored(O o, {Transaction? transaction}) { + var id = entityHandler.getID(o); + return id != null; + } + @override dynamic store(O o, {Transaction? transaction}) { checkNotClosed(); - transaction ??= Transaction.autoCommit(); - - var op = TransactionOperationStore(o); - transaction.addOperation(op); + var op = TransactionOperationStore(o, transaction); - return ensureReferencesStored(o, transaction: transaction).resolveWith(() { + return ensureReferencesStored(o, transaction: op.transaction) + .resolveWith(() { var oId = getID(o, entityHandler: entityHandler); if (oId == null) { oId = nextID(); setID(o, oId, entityHandler: entityHandler); put(o); - transaction!._markOperationExecuted(op, oId); } + op.transaction._markOperationExecuted(op, oId); + return oId; }); } @@ -1577,6 +1738,53 @@ abstract class IterableEntityRepository extends EntityRepository return os.map((o) => store(o, transaction: transaction)).toList(); } + @override + FutureOr setRelationship(O o, String field, List values, + {TypeInfo? fieldType, Transaction? transaction}) { + checkNotClosed(); + + fieldType ??= entityHandler.getFieldType(o, field)!; + + var op = TransactionOperationStoreRelationship(o, values, transaction); + + var valuesType = fieldType.listEntityType.type; + var valuesRepository = provider.getEntityRepository(type: valuesType)!; + + var oId = getID(o, entityHandler: entityHandler); + + var valuesIds = values.map((e) => valuesRepository.entityHandler.getID(e)); + + var valuesIdsNotNull = IterableNullableExtension(valuesIds).whereNotNull(); + + return putRelationship(oId, valuesType, valuesIdsNotNull) + .resolveMapped((ok) { + op.transaction._markOperationExecuted(op, ok); + return ok; + }); + } + + FutureOr putRelationship( + Object oId, Type valuesType, Iterable valuesIds); + + @override + FutureOr> selectRelationship(O? o, String field, + {Object? oId, TypeInfo? fieldType, Transaction? transaction}) { + checkNotClosed(); + + fieldType ??= entityHandler.getFieldType(o, field)!; + oId ??= getID(o!, entityHandler: entityHandler)!; + var valuesType = fieldType.listEntityType.type; + + var op = TransactionOperationSelectRelationship(o ?? oId, transaction); + + var valuesIds = getRelationship(oId!, valuesType); + op.transaction._markOperationExecuted(op, valuesIds); + + return valuesIds; + } + + List getRelationship(Object oId, Type valuesType); + @override FutureOr ensureStored(O o, {Transaction? transaction}) { checkNotClosed(); @@ -1605,14 +1813,31 @@ abstract class IterableEntityRepository extends EntityRepository var value = entityHandler.getField(o, fieldName); if (value == null) return null; - if (!EntityHandler.isValidType(value.runtimeType)) { + var fieldType = entityHandler.getFieldType(o, fieldName)!; + + if (!EntityHandler.isValidType(fieldType.type)) { return null; } - var repository = provider.getEntityRepository(obj: value); - if (repository == null) return null; + if (value is List && fieldType.isList && fieldType.hasArguments) { + var elementType = fieldType.arguments.first; + + var elementRepository = + provider.getEntityRepository(type: elementType.type); + if (elementRepository == null) return null; + + var futures = value.map((e) { + return elementRepository.ensureStored(e, transaction: transaction); + }).toList(); + + return futures.resolveAll(); + } else { + var repository = + provider.getEntityRepository(type: fieldType.type, obj: value); + if (repository == null) return null; - return repository.ensureStored(value, transaction: transaction); + return repository.ensureStored(value, transaction: transaction); + } }); return futures.resolveAllWithValue(true); @@ -1692,4 +1917,27 @@ class SetEntityRepository extends IterableEntityRepository { void remove(O o) { _entries.remove(o); } + + final Map>> _relationships = + >>{}; + + @override + FutureOr putRelationship( + Object oId, Type valuesType, Iterable valuesIds) { + var typeRelationships = + _relationships.putIfAbsent(valuesType, () => >{}); + + typeRelationships[oId] = valuesIds.toSet(); + + return true; + } + + @override + List getRelationship(Object oId, Type valuesType) { + var typeRelationships = _relationships[valuesType]; + + var idReferences = typeRelationships?[oId]; + + return idReferences?.toList() ?? []; + } } diff --git a/lib/src/bones_api_entity_adapter.dart b/lib/src/bones_api_entity_adapter.dart index 0a12c79..22ad879 100644 --- a/lib/src/bones_api_entity_adapter.dart +++ b/lib/src/bones_api_entity_adapter.dart @@ -28,8 +28,16 @@ class SQL { final String? idFieldName; + final Set? returnColumns; + + final String? mainTable; + SQL(this.sql, this.parameters, - {this.condition, this.entityName, this.idFieldName}); + {this.condition, + this.entityName, + this.idFieldName, + this.returnColumns, + required this.mainTable}); @override String toString() { @@ -41,6 +49,12 @@ class SQL { if (entityName != null) { s += ' ; entityName: $entityName'; } + if (mainTable != null) { + s += ' ; mainTable: $mainTable'; + } + if (returnColumns != null && returnColumns!.isNotEmpty) { + s += ' ; returnColumns: $returnColumns'; + } return s; } @@ -86,7 +100,7 @@ abstract class SQLAdapter extends SchemeProvider Map? namedParameters}) { if (matcher == null) { var sqlQuery = 'SELECT count(*) as "count" FROM "$table" '; - return SQL(sqlQuery, {}); + return SQL(sqlQuery, {}, mainTable: table); } else { return _generateSQLFrom(transaction, table, matcher, parameters: parameters, @@ -176,7 +190,9 @@ abstract class SQLAdapter extends SchemeProvider var sqlQuery = sqlBuilder(from, encodedSQL); return SQL(sqlQuery, encodedSQL.parametersPlaceholders, - condition: matcher, entityName: encodedSQL.entityName); + condition: matcher, + entityName: encodedSQL.entityName, + mainTable: table); } else { var referencedTablesFields = encodedSQL.referencedTablesFields; @@ -220,7 +236,9 @@ abstract class SQLAdapter extends SchemeProvider var sqlQuery = sqlBuilder(from, encodedSQL); return SQL(sqlQuery, encodedSQL.parametersPlaceholders, - condition: matcher, entityName: encodedSQL.entityName); + condition: matcher, + entityName: encodedSQL.entityName, + mainTable: table); } }); } else { @@ -234,6 +252,12 @@ abstract class SQLAdapter extends SchemeProvider /// If `true` indicates that this adapter SQL uses the `RETURNING` syntax for inserts. bool get sqlAcceptsInsertReturning; + /// If `true` indicates that this adapter SQL uses the `IGNORE` syntax for inserts. + bool get sqlAcceptsInsertIgnore; + + /// If `true` indicates that this adapter SQL uses the `ON CONFLICT` syntax for inserts. + bool get sqlAcceptsInsertOnConflict; + FutureOr generateInsertSQL( Transaction transaction, String table, Map fields) { var retTableScheme = getTableScheme(table); @@ -285,7 +309,74 @@ abstract class SQLAdapter extends SchemeProvider } return SQL(sql.toString(), fieldsValuesInSQL, - entityName: table, idFieldName: idFieldName); + entityName: table, idFieldName: idFieldName, mainTable: table); + }); + }); + } + + FutureOr generateUpdateSQL(Transaction transaction, String table, + Object id, Map fields) { + var retTableScheme = getTableScheme(table); + + return retTableScheme.resolveMapped((tableScheme) { + if (tableScheme == null) { + throw StateError("Can't find TableScheme for table: $table"); + } + + var context = EncodingContext(table, + namedParameters: fields, transaction: transaction); + + var idFieldName = tableScheme.idFieldName!; + var idPlaceholder = + _conditionSQLGenerator.parameterPlaceholder(idFieldName); + + var fieldsValues = tableScheme.getFieldsValues(fields); + + var fieldsNotNull = fieldsValues.entries + .map((e) => e.value != null && e.key != idFieldName ? e.key : null) + .whereNotNull() + .toList(growable: false); + + var fieldsValuesInSQL = {idFieldName: id}; + + return fieldsNotNull + .map((f) => fieldValueToSQL( + context, tableScheme, f, fieldsValues[f]!, fieldsValuesInSQL)) + .toList(growable: false) + .resolveAll() + .resolveMapped((values) { + var sql = StringBuffer(); + + sql.write('UPDATE "'); + sql.write(table); + sql.write('" SET '); + + for (var i = 0; i < values.length; ++i) { + var f = fieldsNotNull[i]; + var v = values[i]; + + if (i > 0) sql.write(' , '); + sql.write(f); + sql.write(' = '); + sql.write(v); + } + + if (sqlAcceptsInsertOutput) { + sql.write(' OUTPUT INSERTED.'); + sql.write(idFieldName); + } + + sql.write(' WHERE '); + sql.write(idFieldName); + sql.write(' = '); + sql.write(idPlaceholder); + + if (sqlAcceptsInsertReturning) { + sql.write(' RETURNING "$table"."$idFieldName"'); + } + + return SQL(sql.toString(), fieldsValuesInSQL, + entityName: table, idFieldName: idFieldName, mainTable: table); }); }); } @@ -351,9 +442,8 @@ abstract class SQLAdapter extends SchemeProvider } } - FutureOr countSQL( - Transaction transaction, TransactionOperation op, String table, SQL sql) { - return executeTransaction(transaction, op, (connection) { + FutureOr countSQL(TransactionOperation op, String table, SQL sql) { + return executeTransactionOperation(op, (connection) { _log.log(logging.Level.INFO, 'countSQL> $sql'); return doCountSQL(table, sql, connection); }); @@ -365,12 +455,12 @@ abstract class SQLAdapter extends SchemeProvider C connection, ); - FutureOr insertSQL(Transaction transaction, TransactionOperation op, - String table, SQL sql, Map fields, + FutureOr insertSQL(TransactionOperation op, String table, SQL sql, + Map fields, {T Function(dynamic o)? mapper}) { - return executeTransaction(transaction, op, (connection) { + return executeTransactionOperation(op, (connection) { _log.log(logging.Level.INFO, 'insertSQL> $sql'); - var retInsert = doInsertSQL(transaction, table, sql, connection); + var retInsert = doInsertSQL(table, sql, connection); if (mapper != null) { return retInsert.resolveMapped((e) => mapper(e)); @@ -380,17 +470,225 @@ abstract class SQLAdapter extends SchemeProvider }); } - FutureOr executeTransaction(Transaction transaction, + FutureOr updateSQL(TransactionOperation op, String table, SQL sql, + Object id, Map fields, + {T Function(dynamic o)? mapper}) { + return executeTransactionOperation(op, (connection) { + _log.log(logging.Level.INFO, 'updateSQL> $sql'); + var retInsert = doInsertSQL(table, sql, connection); + + if (mapper != null) { + return retInsert.resolveMapped((e) => mapper(e)); + } else { + return retInsert; + } + }); + } + + FutureOr doUpdateSQL(String table, SQL sql, C connection); + + FutureOr> generateInsertRelationshipSQLs(Transaction transaction, + String table, dynamic id, String otherTableName, List otherIds) { + var retTableScheme = getTableScheme(table); + + return retTableScheme.resolveMapped((tableScheme) { + if (tableScheme == null) { + throw StateError("Can't find TableScheme for table: $table"); + } + + var relationship = + tableScheme.getTableRelationshipReference(otherTableName); + + if (relationship == null) { + throw StateError( + "Can't find TableRelationshipReference for tables: $table -> $otherTableName"); + } + + var sqls = otherIds + .map((otherId) => + _generateInsertRelationshipSQL(relationship, id, otherId)) + .toList(); + return sqls; + }); + } + + SQL _generateInsertRelationshipSQL( + TableRelationshipReference relationship, dynamic id, dynamic otherId) { + var relationshipTable = relationship.relationshipTable; + var sourceIdField = relationship.sourceRelationshipField; + var targetIdField = relationship.targetRelationshipField; + + var parameters = {sourceIdField: id, targetIdField: otherId}; + + var sql = StringBuffer(); + + sql.write('INSERT '); + + if (sqlAcceptsInsertIgnore) { + sql.write('IGNORE '); + } + + sql.write('INTO "'); + sql.write(relationshipTable); + sql.write('" ("'); + sql.write(sourceIdField); + sql.write('" , "'); + sql.write(targetIdField); + sql.write('")'); + sql.write(' VALUES ( @$sourceIdField , @$targetIdField )'); + + if (sqlAcceptsInsertOnConflict) { + sql.write(' ON CONFLICT DO NOTHING '); + } + + return SQL(sql.toString(), parameters, mainTable: relationshipTable); + } + + FutureOr insertRelationshipSQLs(TransactionOperation op, String table, + List sqls, dynamic id, String otherTable, List otherIds) { + return executeTransactionOperation(op, (connection) { + _log.log(logging.Level.INFO, + 'insertRelationship>${sqls.length == 1 ? ' ' : '\n - '}${sqls.join('\n -')}'); + + var retInserts = sqls + .map((sql) => doInsertSQL(sql.mainTable ?? table, sql, connection)) + .resolveAll(); + return retInserts.resolveWithValue(true); + }); + } + + FutureOr generateConstrainRelationshipSQL(Transaction transaction, + String table, dynamic id, String otherTableName, List otherIds) { + var retTableScheme = getTableScheme(table); + + return retTableScheme.resolveMapped((tableScheme) { + if (tableScheme == null) { + throw StateError("Can't find TableScheme for table: $table"); + } + + var relationship = + tableScheme.getTableRelationshipReference(otherTableName); + + if (relationship == null) { + throw StateError( + "Can't find TableRelationshipReference for tables: $table -> $otherTableName"); + } + + var relationshipTable = relationship.relationshipTable; + var sourceIdField = relationship.sourceRelationshipField; + var targetIdField = relationship.targetRelationshipField; + + var parameters = {sourceIdField: id}; + + var otherIdsParameters = []; + + var keyPrefix = sourceIdField != 'p' ? 'p' : 'i'; + + for (var otherId in otherIds) { + var i = otherIdsParameters.length + 1; + var key = '$keyPrefix$i'; + parameters[key] = otherId; + otherIdsParameters.add('@$key'); + } + + var sql = StringBuffer(); + + sql.write('DELETE FROM "'); + sql.write(relationshipTable); + sql.write('" WHERE ("'); + sql.write(sourceIdField); + sql.write('" = @$sourceIdField AND "'); + sql.write(targetIdField); + sql.write('" NOT IN ( ${otherIdsParameters.join(',')} ) )'); + + var condition = GroupConditionAND([ + KeyConditionEQ([ConditionKeyField(sourceIdField)], id), + KeyConditionNotIN([ConditionKeyField(targetIdField)], otherIds), + ]); + + return SQL(sql.toString(), parameters, + condition: condition, mainTable: relationshipTable); + }); + } + + FutureOr executeConstrainRelationshipSQL(TransactionOperation op, + String table, SQL sql, dynamic id, String otherTable, List otherIds) { + return executeTransactionOperation(op, (connection) { + _log.log(logging.Level.INFO, 'executeConstrainRelationshipSQL> $sql'); + + var ret = doConstrainSQL( + sql.mainTable ?? table, sql, connection, id, otherTable, otherIds); + return ret; + }); + } + + FutureOr doConstrainSQL(String table, SQL sql, C connection, dynamic id, + String otherTable, List otherIds); + + FutureOr generateSelectRelationshipSQL(Transaction transaction, + String table, dynamic id, String otherTableName) { + var retTableScheme = getTableScheme(table); + + return retTableScheme.resolveMapped((tableScheme) { + if (tableScheme == null) { + throw StateError("Can't find TableScheme for table: $table"); + } + + var relationship = + tableScheme.getTableRelationshipReference(otherTableName); + + if (relationship == null) { + throw StateError( + "Can't find TableRelationshipReference for tables: $table -> $otherTableName"); + } + + var parameters = {'source_id': id}; + + var sql = StringBuffer(); + + sql.write('SELECT "'); + sql.write(relationship.targetRelationshipField); + sql.write('" FROM "'); + sql.write(relationship.relationshipTable); + sql.write('" WHERE ("'); + sql.write(relationship.sourceRelationshipField); + sql.write('" = @source_id '); + sql.write(' )'); + + var condition = KeyConditionEQ( + [ConditionKeyField(relationship.sourceRelationshipField)], id); + + return SQL(sql.toString(), parameters, + condition: condition, + returnColumns: {relationship.targetRelationshipField}, + mainTable: relationship.relationshipTable); + }); + } + + FutureOr>> selectRelationshipSQL( + TransactionOperation op, + String table, + SQL sql, + dynamic id, + String otherTable) { + return executeTransactionOperation(op, (connection) { + _log.log(logging.Level.INFO, 'selectRelationshipSQL> $sql'); + + var ret = doSelectSQL(sql.mainTable ?? table, sql, connection); + return ret; + }); + } + + FutureOr executeTransactionOperation( TransactionOperation op, FutureOr Function(C connection) f) => executeWithPool(f); - FutureOr doInsertSQL( - Transaction transaction, String table, SQL sql, C connection); + FutureOr doInsertSQL(String table, SQL sql, C connection); FutureOr>> selectSQL( - Transaction transaction, TransactionOperation op, String table, SQL sql, + TransactionOperation op, String table, SQL sql, {Map Function(Map r)? mapper}) { - return executeTransaction(transaction, op, (connection) { + return executeTransactionOperation(op, (connection) { _log.log(logging.Level.INFO, 'selectSQL> $sql'); var retSel = doSelectSQL(table, sql, connection); @@ -410,9 +708,9 @@ abstract class SQLAdapter extends SchemeProvider ); FutureOr>> deleteSQL( - Transaction transaction, TransactionOperation op, String table, SQL sql, + TransactionOperation op, String table, SQL sql, {Map Function(Map r)? mapper}) { - return executeTransaction(transaction, op, (connection) { + return executeTransactionOperation(op, (connection) { _log.log(logging.Level.INFO, 'deleteSQL> $sql'); var retSel = doDeleteSQL(table, sql, connection); @@ -479,7 +777,7 @@ abstract class SQLAdapter extends SchemeProvider final Map _repositoriesAdapters = {}; - SQLRepositoryAdapter? getRepositoryAdapter(String name, + SQLRepositoryAdapter? createRepositoryAdapter(String name, {String? tableName, Type? type}) { if (isClosed) { return null; @@ -491,6 +789,27 @@ abstract class SQLAdapter extends SchemeProvider tableName: tableName, type: type)) as SQLRepositoryAdapter; } + SQLRepositoryAdapter? getRepositoryAdapterByName( + String name, + ) { + if (isClosed) return null; + return _repositoriesAdapters[name] as SQLRepositoryAdapter?; + } + + SQLRepositoryAdapter? getRepositoryAdapterByType(Type type) { + if (isClosed) return null; + return _repositoriesAdapters.values.firstWhereOrNull((e) => e.type == type) + as SQLRepositoryAdapter?; + } + + SQLRepositoryAdapter? getRepositoryAdapterByTableName( + String tableName) { + if (isClosed) return null; + return _repositoriesAdapters.values + .firstWhereOrNull((e) => e.tableName == tableName) + as SQLRepositoryAdapter?; + } + final Map _entityRepositories = {}; @@ -591,6 +910,8 @@ abstract class SQLAdapter extends SchemeProvider } } +typedef PreFinishSQLOperation = FutureOr Function(T result); + class SQLRepositoryAdapter with Initializable { final SQLAdapter databaseAdapter; @@ -612,6 +933,11 @@ class SQLRepositoryAdapter with Initializable { String get dialect => databaseAdapter.dialect; + SchemeProvider get schemeProvider => databaseAdapter; + + FutureOr getTableScheme() => + databaseAdapter.getTableScheme(tableName).resolveMapped((t) => t!); + FutureOr generateCountSQL(Transaction transaction, {EntityMatcher? matcher, Object? parameters, @@ -623,9 +949,25 @@ class SQLRepositoryAdapter with Initializable { positionalParameters: positionalParameters, namedParameters: namedParameters); - FutureOr countSQL( - Transaction transaction, TransactionOperation op, SQL sql) { - return databaseAdapter.countSQL(transaction, op, tableName, sql); + FutureOr countSQL(TransactionOperation op, SQL sql) { + return databaseAdapter.countSQL(op, tableName, sql); + } + + FutureOr doCount(TransactionOperation op, + {EntityMatcher? matcher, + Object? parameters, + List? positionalParameters, + Map? namedParameters, + PreFinishSQLOperation? preFinish}) { + return generateCountSQL(op.transaction, + matcher: matcher, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters) + .resolveMapped((sql) { + return countSQL(op, sql) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); } FutureOr generateSelectSQL( @@ -641,8 +983,25 @@ class SQLRepositoryAdapter with Initializable { limit: limit); FutureOr>> selectSQL( - Transaction transaction, TransactionOperation op, SQL sql) { - return databaseAdapter.selectSQL(transaction, op, tableName, sql); + TransactionOperation op, SQL sql) { + return databaseAdapter.selectSQL(op, tableName, sql); + } + + FutureOr doSelect(TransactionOperation op, EntityMatcher matcher, + {Object? parameters, + List? positionalParameters, + Map? namedParameters, + int? limit, + PreFinishSQLOperation>, R>? preFinish}) { + return generateSelectSQL(op.transaction, matcher, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters, + limit: limit) + .resolveMapped((sql) { + return selectSQL(op, sql) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); } FutureOr generateInsertSQL( @@ -650,14 +1009,128 @@ class SQLRepositoryAdapter with Initializable { return databaseAdapter.generateInsertSQL(transaction, tableName, fields); } - FutureOr insertSQL(Transaction transaction, TransactionOperation op, - SQL sql, Map fields, + FutureOr insertSQL( + TransactionOperation op, SQL sql, Map fields, + {String? idFieldName}) { + return databaseAdapter + .insertSQL(op, tableName, sql, fields) + .resolveMapped((ret) => ret ?? {}); + } + + FutureOr doInsert( + TransactionOperation op, O o, Map fields, + {String? idFieldName, + PreFinishSQLOperation? preFinish}) { + return generateInsertSQL(op.transaction, o, fields).resolveMapped((sql) { + return insertSQL(op, sql, fields, idFieldName: idFieldName) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); + } + + FutureOr generateUpdateSQL( + Transaction transaction, O o, Object id, Map fields) { + return databaseAdapter.generateUpdateSQL( + transaction, tableName, id, fields); + } + + FutureOr updateSQL( + TransactionOperation op, SQL sql, Object id, Map fields, {String? idFieldName}) { return databaseAdapter - .insertSQL(transaction, op, tableName, sql, fields) + .updateSQL(op, tableName, sql, id, fields) .resolveMapped((ret) => ret ?? {}); } + FutureOr doUpdate( + TransactionOperation op, O o, Object id, Map fields, + {String? idFieldName, + PreFinishSQLOperation? preFinish}) { + return generateUpdateSQL(op.transaction, o, id, fields) + .resolveMapped((sql) { + return updateSQL(op, sql, id, fields, idFieldName: idFieldName) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); + } + + FutureOr> generateInsertRelationshipSQLs(Transaction transaction, + dynamic id, String otherTableName, List otherIds) { + return databaseAdapter.generateInsertRelationshipSQLs( + transaction, tableName, id, otherTableName, otherIds); + } + + FutureOr insertRelationshipSQLs(TransactionOperation op, List sqls, + dynamic id, String otherTableName, List otherIds) { + return databaseAdapter.insertRelationshipSQLs( + op, tableName, sqls, id, otherTableName, otherIds); + } + + FutureOr doInsertRelationship( + TransactionOperation op, dynamic id, String otherTableName, List otherIds, + [PreFinishSQLOperation? preFinish]) { + return generateInsertRelationshipSQLs( + op.transaction, id, otherTableName, otherIds) + .resolveMapped((sqls) { + return insertRelationshipSQLs(op, sqls, id, otherTableName, otherIds) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); + } + + FutureOr generateConstrainRelationshipSQL(Transaction transaction, + dynamic id, String otherTableName, List othersIds) { + return databaseAdapter.generateConstrainRelationshipSQL( + transaction, tableName, id, otherTableName, othersIds); + } + + FutureOr executeConstrainRelationshipSQL(TransactionOperation op, + SQL sql, dynamic id, String otherTableName, List otherIds) { + return databaseAdapter.executeConstrainRelationshipSQL( + op, tableName, sql, id, otherTableName, otherIds); + } + + FutureOr doConstrainRelationship(TransactionOperation op, dynamic id, + String otherTableName, List othersIds, + [PreFinishSQLOperation? preFinish]) { + return databaseAdapter + .generateConstrainRelationshipSQL( + op.transaction, tableName, id, otherTableName, othersIds) + .resolveMapped((sql) { + return executeConstrainRelationshipSQL( + op, sql, id, otherTableName, othersIds) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); + } + + FutureOr _finishOperation( + TransactionOperation op, T res, PreFinishSQLOperation? preFinish) { + if (preFinish != null) { + return preFinish(res).resolveMapped((res2) => op.finish(res2)); + } else { + return op.finish(res as R); + } + } + + FutureOr generateSelectRelationshipSQL( + Transaction transaction, dynamic id, String otherTableName) { + return databaseAdapter.generateSelectRelationshipSQL( + transaction, tableName, id, otherTableName); + } + + FutureOr>> selectRelationshipSQL( + TransactionOperation op, SQL sql, dynamic id, String otherTableName) { + return databaseAdapter.selectRelationshipSQL( + op, tableName, sql, id, otherTableName); + } + + FutureOr doSelectRelationship( + TransactionOperation op, dynamic id, String otherTableName, + [PreFinishSQLOperation>, R>? preFinish]) { + return generateSelectRelationshipSQL(op.transaction, id, otherTableName) + .resolveMapped((sql) { + return selectRelationshipSQL(op, sql, id, otherTableName) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); + } + FutureOr generateDeleteSQL( Transaction transaction, EntityMatcher matcher, {Object? parameters, @@ -669,7 +1142,22 @@ class SQLRepositoryAdapter with Initializable { namedParameters: namedParameters); FutureOr>> deleteSQL( - Transaction transaction, TransactionOperation op, SQL sql) { - return databaseAdapter.deleteSQL(transaction, op, tableName, sql); + TransactionOperation op, SQL sql) { + return databaseAdapter.deleteSQL(op, tableName, sql); + } + + FutureOr doDelete(TransactionOperation op, EntityMatcher matcher, + {Object? parameters, + List? positionalParameters, + Map? namedParameters, + PreFinishSQLOperation>, R>? preFinish}) { + return generateDeleteSQL(op.transaction, matcher, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters) + .resolveMapped((sql) { + return deleteSQL(op, sql) + .resolveMapped((r) => _finishOperation(op, r, preFinish)); + }); } } diff --git a/lib/src/bones_api_entity_adapter_memory.dart b/lib/src/bones_api_entity_adapter_memory.dart index 51e800b..337bbe7 100644 --- a/lib/src/bones_api_entity_adapter_memory.dart +++ b/lib/src/bones_api_entity_adapter_memory.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:async_extension/async_extension.dart'; import 'package:collection/collection.dart'; import 'bones_api_condition_encoder.dart'; @@ -59,8 +60,7 @@ class MemorySQLAdapter extends SQLAdapter { } @override - FutureOr doInsertSQL( - Transaction transaction, String table, SQL sql, int connection) { + FutureOr doInsertSQL(String table, SQL sql, int connection) { var map = _getTableMap(table, true)!; var id = nextID(table); @@ -77,6 +77,27 @@ class MemorySQLAdapter extends SQLAdapter { return id; } + @override + FutureOr doUpdateSQL(String table, SQL sql, int connection) { + var map = _getTableMap(table, true)!; + + var tablesScheme = tablesSchemes[table]; + var idField = tablesScheme?.idFieldName ?? 'id'; + + var entry = sql.parameters; + var id = entry[idField]; + + map[id] = entry; + + return id; + } + + @override + FutureOr doConstrainSQL(String table, SQL sql, int connection, + dynamic id, String otherTable, List otherIds) { + return doDeleteSQL(table, sql, connection).resolveWithValue(true); + } + Object nextID(String table) { return _tablesIdCount.update(table, (n) => n + 1, ifAbsent: () => 1); } @@ -94,20 +115,40 @@ class MemorySQLAdapter extends SQLAdapter { var entityHandler = getEntityRepository(name: table)?.entityHandler; if (tableScheme == null || tableScheme.fieldsReferencedTables.isEmpty) { - return map.values.where((e) { + var sel = map.values.where((e) { return sql.condition!.matchesEntityMap(e, namedParameters: sql.parameters, entityHandler: entityHandler); }).toList(); + + sel = _filterReturnColumns(sql, sel); + return sel; } var fieldsReferencedTables = tableScheme.fieldsReferencedTables; - return map.values.where((obj) { + var sel = map.values.where((obj) { obj = _resolveEntityMap(obj, fieldsReferencedTables); return sql.condition!.matchesEntityMap(obj, namedParameters: sql.parameters, entityHandler: entityHandler); }).toList(); + + sel = _filterReturnColumns(sql, sel); + return sel; + } + + List> _filterReturnColumns( + SQL sql, List> sel) { + var returnColumns = sql.returnColumns; + + if (returnColumns != null && returnColumns.isNotEmpty) { + return sel.map((e) { + return Map.fromEntries( + e.entries.where((e) => returnColumns.contains(e.key))); + }).toList(); + } else { + return sel; + } } @override @@ -210,6 +251,12 @@ class MemorySQLAdapter extends SQLAdapter { @override bool get sqlAcceptsInsertReturning => true; + @override + bool get sqlAcceptsInsertIgnore => true; + + @override + bool get sqlAcceptsInsertOnConflict => false; + @override String toString() { var tablesSizes = _tables.map((key, value) => MapEntry(key, value.length)); diff --git a/lib/src/bones_api_entity_adapter_postgre.dart b/lib/src/bones_api_entity_adapter_postgre.dart index 993a66c..66c49c3 100644 --- a/lib/src/bones_api_entity_adapter_postgre.dart +++ b/lib/src/bones_api_entity_adapter_postgre.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:async_extension/async_extension.dart'; +import 'package:bones_api/bones_api.dart'; import 'package:bones_api/src/bones_api_condition_encoder.dart'; import 'package:logging/logging.dart' as logging; import 'package:postgres/postgres.dart'; @@ -145,15 +146,16 @@ class PostgreSQLAdapter extends SQLAdapter { return MapEntry(k, v); })); - var fieldsNames = fieldsTypes.keys.toList(growable: false); - var fieldsReferencedTables = - await _findFieldsReferencedTables(connection, table, fieldsNames); + await _findFieldsReferencedTables(connection, table); + + var relationshipTables = + await _findRelationshipTables(connection, table, idFieldName); await releaseIntoPool(connection); - var tableScheme = - TableScheme(table, idFieldName, fieldsTypes, fieldsReferencedTables); + var tableScheme = TableScheme(table, idFieldName, fieldsTypes, + fieldsReferencedTables, relationshipTables); _log.log(logging.Level.INFO, '$tableScheme'); @@ -206,10 +208,73 @@ class PostgreSQLAdapter extends SQLAdapter { } } - Future> _findFieldsReferencedTables( + Future> _findRelationshipTables( PostgreSQLExecutionContext connection, String table, - List fieldsNames) async { + String idFieldName) async { + var tablesNames = await _listTablesNames(connection); + + var tablesReferences = await tablesNames + .map((t) => _findFieldsReferencedTables(connection, t)) + .resolveAll(); + + tablesReferences = tablesReferences.where((m) { + return m.length > 1 && + m.values.where((r) => r.targetTable == table).isNotEmpty && + m.values.where((r) => r.targetTable != table).isNotEmpty; + }).toList(); + + var relationships = tablesReferences.map((e) { + var refToTable = e.values.where((r) => r.targetTable == table).first; + var otherRef = e.values.where((r) => r.targetTable != table).first; + return TableRelationshipReference( + refToTable.sourceTable, + refToTable.targetTable, + refToTable.targetField, + refToTable.sourceField, + otherRef.targetTable, + otherRef.targetField, + otherRef.sourceField, + ); + }).toList(); + + return relationships; + } + + Future> _listTablesNames( + PostgreSQLExecutionContext connection) async { + var sql = ''' + SELECT table_name FROM information_schema.tables WHERE table_schema='public' + '''; + + var results = await connection.mappedResultsQuery(sql); + + var names = results + .map((e) { + var v = e.values.first; + if (v is Map) { + return v.values.first; + } else { + return v; + } + }) + .map((e) => '$e') + .toList(); + + return names; + } + + final TimedMap> + _findFieldsReferencedTablesCache = + TimedMap>(Duration(seconds: 30)); + + FutureOr> _findFieldsReferencedTables( + PostgreSQLExecutionContext connection, String table) => + _findFieldsReferencedTablesCache.putIfAbsentCheckedAsync( + table, () => _findFieldsReferencedTablesImpl(connection, table)); + + Future> _findFieldsReferencedTablesImpl( + PostgreSQLExecutionContext connection, String table) async { var sql = ''' SELECT o.conname AS constraint_name, @@ -301,41 +366,73 @@ class PostgreSQLAdapter extends SQLAdapter { bool get sqlAcceptsInsertReturning => true; @override - FutureOr doInsertSQL(Transaction transaction, String table, SQL sql, - PostgreSQLExecutionContext connection) { + bool get sqlAcceptsInsertIgnore => false; + + @override + bool get sqlAcceptsInsertOnConflict => true; + + @override + FutureOr doInsertSQL( + String table, SQL sql, PostgreSQLExecutionContext connection) { return connection .mappedResultsQuery(sql.sql, substitutionValues: sql.parameters) - .resolveMapped((results) { - if (results.isEmpty) { - return null; - } + .resolveMapped((results) => _resolveResultID(results, table, sql)); + } - var returning = results.first[table]; + @override + FutureOr doUpdateSQL( + String table, SQL sql, PostgreSQLExecutionContext connection) { + return connection + .mappedResultsQuery(sql.sql, substitutionValues: sql.parameters) + .resolveMapped((results) => _resolveResultID(results, table, sql)); + } - if (returning == null || returning.isEmpty) { - return null; - } else if (returning.length == 1) { - var id = returning.values.first; + _resolveResultID( + List>> results, String table, SQL sql) { + if (results.isEmpty) { + return null; + } + + var returning = results.first[table]; + + if (returning == null || returning.isEmpty) { + return null; + } else if (returning.length == 1) { + var id = returning.values.first; + return id; + } else { + var idFieldName = sql.idFieldName; + + if (idFieldName != null) { + var id = returning[idFieldName]; return id; } else { - var idFieldName = sql.idFieldName; - - if (idFieldName != null) { - var id = returning[idFieldName]; - return id; - } else { - var id = returning.values.first; - return id; - } + var id = returning.values.first; + return id; } + } + } + + @override + FutureOr doConstrainSQL( + String table, + SQL sql, + PostgreSQLExecutionContext connection, + dynamic id, + String otherTable, + List otherIds) { + return connection + .query(sql.sql, substitutionValues: sql.parameters) + .resolveMapped((result) { + return true; }); } @override - FutureOr executeTransaction( - Transaction transaction, - TransactionOperation op, + FutureOr executeTransactionOperation(TransactionOperation op, TransactionExecution f) { + var transaction = op.transaction; + if (transaction.length == 1) { return executeWithPool((connection) => f(connection)); } diff --git a/lib/src/bones_api_entity_sql.dart b/lib/src/bones_api_entity_sql.dart index 5b1d2f6..65802cf 100644 --- a/lib/src/bones_api_entity_sql.dart +++ b/lib/src/bones_api_entity_sql.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:async_extension/async_extension.dart'; +import 'package:bones_api/bones_api.dart'; +import 'package:collection/collection.dart'; import 'bones_api_condition.dart'; import 'bones_api_entity.dart'; @@ -14,7 +16,7 @@ class SQLEntityRepository extends EntityRepository SQLAdapter adapter, String name, EntityHandler entityHandler, {SQLRepositoryAdapter? repositoryAdapter, Type? type}) : sqlRepositoryAdapter = - repositoryAdapter ?? adapter.getRepositoryAdapter(name)!, + repositoryAdapter ?? adapter.createRepositoryAdapter(name)!, super(adapter, name, entityHandler, type: type); @override @@ -35,7 +37,6 @@ class SQLEntityRepository extends EntityRepository var id = getID(o, entityHandler: entityHandler); if (id == null) { - transaction ??= Transaction.executingOrNew(); return store(o, transaction: transaction); } else { return ensureReferencesStored(o, transaction: transaction) @@ -56,14 +57,30 @@ class SQLEntityRepository extends EntityRepository var value = entityHandler.getField(o, fieldName); if (value == null) return null; - if (!EntityHandler.isValidType(value.runtimeType)) { + var fieldType = entityHandler.getFieldType(o, fieldName)!; + + if (!EntityHandler.isValidType(fieldType.type)) { return null; } - var repository = provider.getEntityRepository(obj: value); - if (repository == null) return null; - - return repository.ensureStored(value, transaction: transaction); + if (value is List && fieldType.isList && fieldType.hasArguments) { + var elementType = fieldType.arguments.first; + var elementRepository = + provider.getEntityRepository(type: elementType.type); + if (elementRepository == null) return null; + + var futures = value.map((e) { + return elementRepository.ensureStored(e, + transaction: transaction); + }).toList(); + return futures.resolveAll(); + } else { + var repository = + provider.getEntityRepository(type: fieldType.type, obj: value); + if (repository == null) return null; + + return repository.ensureStored(value, transaction: transaction); + } }) .whereNotNull() .toList(growable: false); @@ -85,30 +102,17 @@ class SQLEntityRepository extends EntityRepository TransactionOperation? op}) { checkNotClosed(); - var externalTransaction = transaction != null; - transaction ??= Transaction.executingOrNew(); - var transactionRoot = transaction.isEmpty && !transaction.isExecuting; - - var op = TransactionOperationCount(); - transaction.addOperation(op); + var op = TransactionOperationCount(null, transaction); - var retSql = sqlRepositoryAdapter.generateCountSQL(transaction, + return sqlRepositoryAdapter.doCount(op, matcher: matcher, parameters: parameters, positionalParameters: positionalParameters, namedParameters: namedParameters); - - return retSql.resolveMapped((sql) { - var retCount = sqlRepositoryAdapter.countSQL(transaction!, op, sql); - return retCount.resolveMapped((count) { - return transaction! - .finishOperation(op, count, transactionRoot, externalTransaction); - }); - }); } @override - FutureOr> select(EntityMatcher matcher, + FutureOr> select(EntityMatcher matcher, {Object? parameters, List? positionalParameters, Map? namedParameters, @@ -116,67 +120,263 @@ class SQLEntityRepository extends EntityRepository int? limit}) { checkNotClosed(); - var externalTransaction = transaction != null; - transaction ??= Transaction.executingOrNew(); - var transactionRoot = transaction.isEmpty && !transaction.isExecuting; - - var op = TransactionOperationSelect(matcher); - transaction.addOperation(op); + var op = TransactionOperationSelect(matcher, transaction); - var retSql = sqlRepositoryAdapter.generateSelectSQL(transaction, matcher, + return sqlRepositoryAdapter.doSelect(op, matcher, parameters: parameters, positionalParameters: positionalParameters, namedParameters: namedParameters, - limit: limit); - - return retSql.resolveMapped((sql) { - var selRet = sqlRepositoryAdapter.selectSQL(transaction!, op, sql); + limit: limit, preFinish: (results) { + return _resolveEntities(op.transaction, results); + }); + } - return selRet.resolveMapped((sel) { - var entities = sel.map((e) => entityHandler.createFromMap(e)).toList(); - return entities.resolveAllJoined((l) { - return transaction! - .finishOperation(op, l, transactionRoot, externalTransaction); - }); + FutureOr> _resolveEntities( + Transaction transaction, Iterable> results) { + if (results.isEmpty) return []; + + var fieldsListEntity = entityHandler.fieldsWithTypeListEntity(); + + if (fieldsListEntity.isNotEmpty) { + var retTableScheme = sqlRepositoryAdapter.getTableScheme(); + var retRelationshipFields = + _getRelationshipFields(fieldsListEntity, retTableScheme); + + var ret = retTableScheme.resolveOther>, + Map>(retRelationshipFields, + (tableScheme, relationshipFields) { + if (relationshipFields.isNotEmpty) { + results = results is List ? results : results.toList(); + if (results.isEmpty) return []; + + var resolveRelationshipsFields = _resolveRelationshipFields( + transaction, + tableScheme, + results, + relationshipFields, + fieldsListEntity, + ); + + return resolveRelationshipsFields + .resolveAllWith(() => _resolveEntitiesSimple(results)); + } else { + return _resolveEntitiesSimple(results); + } }); + + return ret.resolveMapped((entities) => entities.resolveAll()); + } else { + return _resolveEntitiesSimple(results).resolveAll(); + } + } + + Iterable> _resolveRelationshipFields( + Transaction transaction, + TableScheme tableScheme, + Iterable> results, + Map relationshipFields, + Map fieldsListEntity, + ) { + var idFieldName = tableScheme.idFieldName!; + var ids = results.map((e) => e[idFieldName]).toList(); + + var databaseAdapter = sqlRepositoryAdapter.databaseAdapter; + + return relationshipFields.entries.map((e) { + var fieldName = e.key; + var fieldType = fieldsListEntity[fieldName]!; + var targetTable = e.value.targetTable; + + var targetRepositoryAdapter = + databaseAdapter.getRepositoryAdapterByTableName(targetTable)!; + var targetType = targetRepositoryAdapter.type; + var targetEntityRepository = + provider.getEntityRepository(type: targetType)!; + + var retRelationships = Map.fromEntries(ids.map((id) { + var retTargetIds = selectRelationship(null, fieldName, + oId: id, transaction: transaction, fieldType: fieldType); + + var ret = retTargetIds.resolveMapped((targetIds) { + return targetIds + .map((targetId) => targetEntityRepository.selectByID(targetId, + transaction: transaction)) + .resolveAll(); + }).resolveMapped((l) => + targetEntityRepository.entityHandler.castList(l, targetType)!); + + return MapEntry(id, ret); + })).resolveAllValues(); + + return retRelationships.resolveMapped((relationships) { + for (var r in results) { + var id = r[idFieldName]; + var values = relationships[id]; + r[fieldName] = values; + } + }).resolveWithValue(true); + }); + } + + List> _resolveEntitiesSimple( + Iterable> result) => + result.map((e) => entityHandler.createFromMap(e)).toList(); + + FutureOr> _getRelationshipFields( + Map fieldsListEntity, + [FutureOr? retTableScheme]) { + retTableScheme ??= sqlRepositoryAdapter.getTableScheme(); + + return retTableScheme.resolveMapped((tableScheme) { + var databaseAdapter = sqlRepositoryAdapter.databaseAdapter; + + var entries = fieldsListEntity.entries.map((e) { + var targetType = e.value.listEntityType!.type; + var targetRepositoryAdapter = + databaseAdapter.getRepositoryAdapterByType(targetType); + if (targetRepositoryAdapter == null) return null; + var relationship = tableScheme + .getTableRelationshipReference(targetRepositoryAdapter.name); + if (relationship == null) return null; + return MapEntry(e.key, relationship); + }).whereNotNull(); + + return Map.fromEntries(entries); }); } + @override + bool isStored(O o, {Transaction? transaction}) { + var id = entityHandler.getID(o); + return id != null; + } + @override FutureOr store(O o, {Transaction? transaction}) { checkNotClosed(); - var externalTransaction = transaction != null; - transaction ??= Transaction.executingOrNew(); - var transactionRoot = transaction.isEmpty && !transaction.isExecuting; + if (isStored(o, transaction: transaction)) { + return _update(o, transaction); + } - var op = TransactionOperationStore(o); - transaction.addOperation(op); + var op = TransactionOperationStore(o, transaction); - return ensureReferencesStored(o, transaction: transaction).resolveWith(() { + return ensureReferencesStored(o, transaction: op.transaction) + .resolveWith(() { + var idFieldsName = entityHandler.idFieldsName(o); var fields = entityHandler.getFields(o); - var retSql = - sqlRepositoryAdapter.generateInsertSQL(transaction!, o, fields); - - return retSql.resolveMapped((sql) { - var retId = - sqlRepositoryAdapter.insertSQL(transaction!, op, sql, fields); - - return retId.resolveMapped((id) { - entityHandler.setID(o, id); - return transaction! - .finishOperation(op, id, transactionRoot, externalTransaction); - }); + + return sqlRepositoryAdapter + .doInsert(op, o, fields, idFieldName: idFieldsName, preFinish: (id) { + entityHandler.setID(o, id); + return _ensureRelationshipsStored(o, op.transaction) + .resolveWithValue(id); }); }); } + FutureOr _update(O o, Transaction? transaction) { + var op = TransactionOperationUpdate(o, transaction); + + return ensureReferencesStored(o, transaction: op.transaction) + .resolveWith(() { + var idFieldsName = entityHandler.idFieldsName(o); + var id = entityHandler.getID(o); + var fields = entityHandler.getFields(o); + + return sqlRepositoryAdapter.doUpdate(op, o, id, fields, + idFieldName: idFieldsName, preFinish: (id) { + return _ensureRelationshipsStored(o, op.transaction) + .resolveWithValue(id); + }); + }); + } + + FutureOr _ensureRelationshipsStored(O o, Transaction? transaction) { + var fieldsListEntity = entityHandler.fieldsWithTypeListEntity(o); + if (fieldsListEntity.isEmpty) return false; + + var ret = fieldsListEntity.entries.map((e) { + var values = entityHandler.getField(o, e.key); + return setRelationship(o, e.key, values, + fieldType: e.value, transaction: transaction); + }).resolveAll(); + + return ret.resolveWithValue(true); + } + + @override + FutureOr setRelationship(O o, String field, List values, + {TypeInfo? fieldType, Transaction? transaction}) { + fieldType ??= entityHandler.getFieldType(o, field)!; + + var op = TransactionOperationStoreRelationship(o, values, transaction); + + var valuesType = fieldType.listEntityType!.type; + String valuesTableName = _resolveTableName(valuesType); + var valuesEntityHandler = _resolveEntityHandler(valuesType); + + var oId = entityHandler.getID(o); + var othersIds = values.map((e) => valuesEntityHandler.getID(e)).toList(); + + return sqlRepositoryAdapter + .doInsertRelationship(op, oId, valuesTableName, othersIds, (ok) { + if (ok) { + var op2 = TransactionOperationConstrainRelationship( + o, values, op.transaction); + + return sqlRepositoryAdapter.doConstrainRelationship( + op2, oId, valuesTableName, othersIds); + } else { + return ok; + } + }); + } + + @override + FutureOr> selectRelationship(O? o, String field, + {Object? oId, TypeInfo? fieldType, Transaction? transaction}) { + fieldType ??= entityHandler.getFieldType(o, field)!; + + oId ??= entityHandler.getID(o!); + + var op = TransactionOperationSelectRelationship(o ?? oId, transaction); + + var valuesType = fieldType.listEntityType!.type; + String valuesTableName = _resolveTableName(valuesType); + + return sqlRepositoryAdapter.doSelectRelationship(op, oId, valuesTableName, + (sel) { + var valuesIds = sel.map((e) => e.values.first).cast().toList(); + return valuesIds; + }); + } + + String _resolveTableName(Type type) { + var repositoryAdapter = + sqlRepositoryAdapter.databaseAdapter.getRepositoryAdapterByType(type)!; + return repositoryAdapter.tableName; + } + + EntityHandler _resolveEntityHandler(Type type) { + var entityRepository = entityHandler.getEntityRepository(type: type); + var entityHandler2 = entityRepository?.entityHandler; + entityHandler2 ??= entityHandler.getEntityHandler(type: type); + if (entityHandler2 == null) { + throw StateError("Can't resolve EntityHandler for type: $type"); + } + return entityHandler2 as EntityHandler; + } + @override Iterable storeAll(Iterable os, {Transaction? transaction}) { checkNotClosed(); transaction ??= Transaction.executingOrNew(); - return os.map((o) => store(o, transaction: transaction)).toList(); + + var result = os.map((o) => store(o, transaction: transaction)).toList(); + + return result; } @override @@ -187,28 +387,13 @@ class SQLEntityRepository extends EntityRepository Transaction? transaction}) { checkNotClosed(); - var externalTransaction = transaction != null; - transaction ??= Transaction.executingOrNew(); - var transactionRoot = transaction.isEmpty && !transaction.isExecuting; - - var op = TransactionOperationDelete(matcher); - transaction.addOperation(op); + var op = TransactionOperationDelete(matcher, transaction); - var retSql = sqlRepositoryAdapter.generateDeleteSQL(transaction, matcher, + return sqlRepositoryAdapter.doDelete(op, matcher, parameters: parameters, positionalParameters: positionalParameters, - namedParameters: namedParameters); - - return retSql.resolveMapped((sql) { - var selRet = sqlRepositoryAdapter.deleteSQL(transaction!, op, sql); - - return selRet.resolveMapped((sel) { - var entities = sel.map((e) => entityHandler.createFromMap(e)).toList(); - return entities.resolveAllJoined((l) { - return transaction! - .finishOperation(op, l, transactionRoot, externalTransaction); - }); - }); + namedParameters: namedParameters, preFinish: (results) { + return _resolveEntities(op.transaction, results); }); } } diff --git a/lib/src/bones_api_utils.dart b/lib/src/bones_api_utils.dart index 34d7e9e..7c9f5c4 100644 --- a/lib/src/bones_api_utils.dart +++ b/lib/src/bones_api_utils.dart @@ -567,6 +567,9 @@ class TimedMap implements Map { final Map _entriesPutTime = {}; + @override + String toString() => _entries.toString(); + /// Sets a [key] [value]. See [put]. @override void operator []=(K key, V value) { @@ -587,7 +590,7 @@ class TimedMap implements Map { } /// Returns the time ([DateTime]) of a [key]. - DateTime? getTime(K key) { + DateTime? getTime(Object? key) { return _entriesPutTime[key]; } @@ -687,7 +690,7 @@ class TimedMap implements Map { /// Returns the elapsed time of [key], since the put. Duration? getElapsedTime(Object? key, {DateTime? now}) { - var time = _entriesPutTime[key]; + var time = getTime(key); if (time == null) return null; now ??= DateTime.now(); diff --git a/pubspec.yaml b/pubspec.yaml index a459e99..e3b03b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A Powerful API backend framework for Dart. Comes with a built-in HTTP Server, routes handler, entity handler, SQL translator, and DB adapters. -version: 1.0.12 +version: 1.0.13 homepage: https://github.com/Colossus-Services/bones_api environment: @@ -14,7 +14,7 @@ dependencies: async_extension: ^1.0.7 dart_spawner: ^1.0.5 args: ^2.2.0 - reflection_factory: ^1.0.10 + reflection_factory: ^1.0.11 petitparser: ^4.2.0 hotreloader: ^3.0.1 logging: ^1.0.1 diff --git a/test/bones_api_entity_test.dart b/test/bones_api_entity_test.dart index 7d3fd3d..def9612 100644 --- a/test/bones_api_entity_test.dart +++ b/test/bones_api_entity_test.dart @@ -17,6 +17,7 @@ class APIEntityRepositoryProvider extends EntityRepositoryProvider { late final MemorySQLAdapter sqlAdapter; late final SQLEntityRepository
addressSQLRepository; + late final SQLEntityRepository roleSQLRepository; late final SQLEntityRepository userSQLRepository; late final AddressAPIRepository addressAPIRepository; @@ -25,30 +26,36 @@ class APIEntityRepositoryProvider extends EntityRepositoryProvider { APIEntityRepositoryProvider._() { sqlAdapter = MemorySQLAdapter(parentRepositoryProvider: this) ..addTableSchemes([ + TableScheme('user', 'id', { + 'id': int, + 'email': String, + 'password': String, + 'address': int, + 'creationTime': DateTime, + }, { + 'address': TableFieldReference('user', 'address', 'address', 'id') + }, [ + TableRelationshipReference( + 'user_role', 'user', 'id', 'user_id', 'role', 'id', 'role_id') + ]), TableScheme( - 'user', + 'address', 'id', { 'id': int, - 'email': String, - 'password': String, - 'address': int, - 'creationTime': DateTime, - }, - { - 'address': - TableFieldReference('account', 'address', 'address', 'id') + 'state': String, + 'city': String, + 'street': String, + 'number': int }, ), TableScheme( - 'address', + 'role', 'id', { 'id': int, - 'state': String, - 'city': String, - 'street': String, - 'number': int + 'type': String, + 'enabled': bool, }, ) ]); @@ -57,6 +64,10 @@ class APIEntityRepositoryProvider extends EntityRepositoryProvider { sqlAdapter, 'address', addressEntityHandler) ..ensureInitialized(); + roleSQLRepository = + SQLEntityRepository(sqlAdapter, 'role', roleEntityHandler) + ..ensureInitialized(); + userSQLRepository = SQLEntityRepository(sqlAdapter, 'user', userEntityHandler) ..ensureInitialized(); @@ -72,6 +83,7 @@ void main() { group('Entity', () { late final SetEntityRepository
addressRepository; + late final SetEntityRepository roleRepository; late final SetEntityRepository userRepository; setUpAll(() { @@ -79,6 +91,9 @@ void main() { SetEntityRepository
('address', addressEntityHandler); addressRepository.ensureInitialized(); + roleRepository = SetEntityRepository('role', roleEntityHandler); + roleRepository.ensureInitialized(); + userRepository = SetEntityRepository('user', userEntityHandler); userRepository.ensureInitialized(); }); @@ -89,19 +104,23 @@ void main() { }); test('basic', () async { - var user1 = User( - 'joe@mail.com', '123', Address('NY', 'New York', 'Fifth Avenue', 101), + var user1 = User('joe@mail.com', '123', + Address('NY', 'New York', 'Fifth Avenue', 101), [Role('admin')], creationTime: DateTime.utc(2020, 10, 11, 12, 13, 14, 0, 0)); - var user2 = User('smith@mail.com', 'abc', + var user2 = User( + 'smith@mail.com', + 'abc', Address('CA', 'Los Angeles', 'Hollywood Boulevard', 404), + [Role('guest')], creationTime: DateTime.utc(2021, 10, 11, 12, 13, 14, 0, 0)); var user1Json = - '{"email":"joe@mail.com","password":"123","address":{"state":"NY","city":"New York","street":"Fifth Avenue","number":101},"creationTime":1602418394000}'; + '{"email":"joe@mail.com","password":"123","address":{"state":"NY","city":"New York","street":"Fifth Avenue","number":101},"roles":[{"type":"admin","enabled":true}],"creationTime":1602418394000}'; var user2Json = - '{"email":"smith@mail.com","password":"abc","address":{"state":"CA","city":"Los Angeles","street":"Hollywood Boulevard","number":404},"creationTime":1633954394000}'; + '{"email":"smith@mail.com","password":"abc","address":{"state":"CA","city":"Los Angeles","street":"Hollywood Boulevard","number":404},"roles":[{"type":"guest","enabled":true}],"creationTime":1633954394000}'; addressEntityHandler.inspectObject(user1.address); + roleEntityHandler.inspectObject(user1.roles.first); userEntityHandler.inspectObject(user1); expect(userEntityHandler.encodeJson(user1), equals(user1Json)); @@ -125,11 +144,14 @@ void main() { var user1Time = DateTime.utc(2019, 1, 2, 3, 4, 5); var user2Time = DateTime.utc(2019, 12, 2, 3, 4, 5); - var user1 = User( - 'joe@setl.com', '123', Address('NY', 'New York', 'Fifth Avenue', 101), + var user1 = User('joe@setl.com', '123', + Address('NY', 'New York', 'Fifth Avenue', 101), [Role('admin')], creationTime: user1Time); - var user2 = User('smith@setl.com', 'abc', + var user2 = User( + 'smith@setl.com', + 'abc', Address('CA', 'Los Angeles', 'Hollywood Boulevard', 404), + [Role('guest')], creationTime: user2Time); userRepository.store(user1); @@ -249,8 +271,8 @@ void main() { { var address = Address('NY', 'New York', 'street A', 101); - var user = - User('joe@memory.com', '123', address, creationTime: user1Time); + var user = User('joe@memory.com', '123', address, [Role('admin')], + creationTime: user1Time); var id = await userSQLRepository.store(user); expect(id, equals(1)); } @@ -260,8 +282,8 @@ void main() { { var address = Address('CA', 'Los Angeles', 'street B', 201); - var user = - User('smith@memory.com', 'abc', address, creationTime: user2Time); + var user = User('smith@memory.com', 'abc', address, [Role('guest')], + creationTime: user2Time); var id = await userSQLRepository.store(user); expect(id, equals(2)); } @@ -278,6 +300,11 @@ void main() { var user = await userSQLRepository.selectByID(1); expect(user!.email, equals('joe@memory.com')); expect(user.address.state, equals('NY')); + expect( + user.roles.map((e) => e.toJson()), + equals([ + {'id': 1, 'type': 'admin', 'enabled': true} + ])); expect(user.creationTime, equals(user1Time)); } @@ -285,6 +312,11 @@ void main() { var user = await userSQLRepository.selectByID(2); expect(user!.email, equals('smith@memory.com')); expect(user.address.state, equals('CA')); + expect( + user.roles.map((e) => e.toJson()), + equals([ + {'id': 2, 'type': 'guest', 'enabled': true} + ])); expect(user.creationTime, equals(user2Time)); } @@ -417,7 +449,12 @@ void main() { // Add User: { var address = Address('FL', 'Miami', 'Ocean Drive', 11); - var user = User('mary@memory.com', 'xyz', address); + var user = User( + 'mary@memory.com', + 'xyz', + address, + [Role('guest')], + ); var id = await userAPIRepository.store(user); expect(id, equals(3)); } @@ -475,7 +512,12 @@ void main() { var t = Transaction(); await t.execute(() async { var address = Address('TX', 'Austin', 'Main street', 22); - var user = User('bill@memory.com', 'txs', address); + var user = User( + 'bill@memory.com', + 'txs', + address, + [Role('guest')], + ); var id = await userAPIRepository.store(user); expect(id, equals(4)); @@ -491,7 +533,7 @@ void main() { }); expect(t.isCommitted, isTrue); - expect(t.length, equals(4)); + expect(t.length, equals(9)); expect(t.isExecuting, isFalse); expect((t.result as List).first, isA()); } diff --git a/test/bones_api_postgre_test.dart b/test/bones_api_postgre_test.dart index 62286c2..f9cbece 100644 --- a/test/bones_api_postgre_test.dart +++ b/test/bones_api_postgre_test.dart @@ -22,6 +22,7 @@ class PostgreEntityRepositoryProvider extends EntityRepositoryProvider { EntityHandler userEntityHandler; late final AddressAPIRepository addressAPIRepository; + late final RoleAPIRepository roleAPIRepository; late final UserAPIRepository userAPIRepository; PostgreEntityRepositoryProvider( @@ -37,10 +38,14 @@ class PostgreEntityRepositoryProvider extends EntityRepositoryProvider { SQLEntityRepository
( postgreAdapter, 'address', addressEntityHandler); + SQLEntityRepository(postgreAdapter, 'role', roleEntityHandler); + SQLEntityRepository(postgreAdapter, 'user', userEntityHandler); addressAPIRepository = AddressAPIRepository(this)..ensureConfigured(); + roleAPIRepository = RoleAPIRepository(this)..ensureConfigured(); + userAPIRepository = UserAPIRepository(this)..ensureConfigured(); } } @@ -142,30 +147,60 @@ void main() { var sqlCreateUser = ''' CREATE TABLE IF NOT EXISTS "user" ( - "id" serial, - "email" text, - "password" text, - "address" integer CONSTRAINT address_ref_account_fk REFERENCES address(id), - "creation_time" timestamp, - PRIMARY KEY( id ) + "id" serial, + "email" text NOT NULL, + "password" text NOT NULL, + "address" integer NOT NULL CONSTRAINT address_ref_account_fk REFERENCES address(id), + "creation_time" timestamp NOT NULL, + PRIMARY KEY( id ) ); '''; var process2 = await postgreContainer.runSQL(sqlCreateUser); expect(process2, contains('CREATE TABLE')); - var process3 = await postgreContainer.psqlCMD('\\d'); + var sqlCreateRole = ''' + CREATE TABLE IF NOT EXISTS "role" ( + "id" serial, + "type" text NOT NULL, + "enabled" boolean NOT NULL, + PRIMARY KEY( id ) + ); + '''; + + var process3 = await postgreContainer.runSQL(sqlCreateRole); + expect(process3, contains('CREATE TABLE')); + + var sqlCreateUserRole = ''' + CREATE TABLE IF NOT EXISTS "user_role_ref" ( + "user_id" integer REFERENCES "user"(id) ON DELETE CASCADE, + "role_id" integer REFERENCES "role"(id) ON DELETE CASCADE, + CONSTRAINT user_role_ref_pkey PRIMARY KEY ("user_id", "role_id") + ); + '''; + + var process4 = await postgreContainer.runSQL(sqlCreateUserRole); + expect(process4, contains('CREATE TABLE')); + + var processList = await postgreContainer.psqlCMD('\\d'); + + print(processList); expect( - process3, + processList, allOf( - contains(RegExp(r'\Waddress\W')), contains(RegExp(r'\Wuser\W')))); + contains(RegExp(r'\Waddress\W')), + contains(RegExp(r'\Wuser\W')), + contains(RegExp(r'\Wrole\W')), + contains(RegExp(r'\Wuser_role_ref\W')), + )); }); test('PostgreEntityRepositoryProvider', () async { if (!checkDockerRunning('PostgreEntityRepositoryProvider')) return; var addressAPIRepository = entityRepositoryProvider.addressAPIRepository; + var roleAPIRepository = entityRepositoryProvider.roleAPIRepository; var userAPIRepository = entityRepositoryProvider.userAPIRepository; expect(await userAPIRepository.length(), equals(0)); @@ -180,13 +215,18 @@ void main() { expect(del, isEmpty); } + { + var role = await roleAPIRepository.selectByID(1); + expect(role, isNull); + } + var user1Time = DateTime.utc(2021, 9, 20, 10, 11, 12, 0, 0); { var address = Address('NY', 'New York', 'street A', 101); - var user = - User('joe@postgre.com', '123', address, creationTime: user1Time); + var user = User('joe@postgre.com', '123', address, [Role('admin')], + creationTime: user1Time); var id = await userAPIRepository.store(user); expect(id, equals(1)); } @@ -195,8 +235,8 @@ void main() { { var address = Address('CA', 'Los Angeles', 'street B', 201); - var user = - User('smith@postgre.com', 'abc', address, creationTime: user2Time); + var user = User('smith@postgre.com', 'abc', address, [Role('guest')], + creationTime: user2Time); var id = await userAPIRepository.store(user); expect(id, equals(2)); } @@ -208,6 +248,11 @@ void main() { var user = await userAPIRepository.selectByID(1); expect(user!.email, equals('joe@postgre.com')); expect(user.address.state, equals('NY')); + expect( + user.roles.map((e) => e.toJson()), + equals([ + {'id': 1, 'type': 'admin', 'enabled': true} + ])); expect(user.creationTime, equals(user1Time)); } @@ -215,6 +260,11 @@ void main() { var user = await userAPIRepository.selectByID(2); expect(user!.email, equals('smith@postgre.com')); expect(user.address.state, equals('CA')); + expect( + user.roles.map((e) => e.toJson()), + equals([ + {'id': 2, 'type': 'guest', 'enabled': true} + ])); expect(user.creationTime, equals(user2Time)); } @@ -245,11 +295,24 @@ void main() { expect(user.address.state, equals('NY')); } + { + var sel = await userAPIRepository.selectByAddressState('CA'); + + var user = sel.first; + expect(user.email, equals('smith@postgre.com')); + expect(user.address.state, equals('CA')); + + user.email = 'smith2@postgre.com'; + + var ok = await userAPIRepository.store(user); + expect(ok, equals(user.id)); + } + { var del = await userAPIRepository .deleteByQuery(' #ID == ? ', parameters: [2]); var user = del.first; - expect(user.email, equals('smith@postgre.com')); + expect(user.email, equals('smith2@postgre.com')); expect(user.address.state, equals('CA')); expect(user.creationTime, equals(user2Time)); } diff --git a/test/bones_api_test.reflection.g.dart b/test/bones_api_test.reflection.g.dart index f94c1e5..96b0937 100644 --- a/test/bones_api_test.reflection.g.dart +++ b/test/bones_api_test.reflection.g.dart @@ -1,6 +1,6 @@ // // GENERATED CODE - DO NOT MODIFY BY HAND! -// BUILDER: reflection_factory/1.0.10 +// BUILDER: reflection_factory/1.0.11 // BUILD COMMAND: dart run build_runner build // @@ -50,7 +50,7 @@ class MyInfoModule$reflection extends ClassReflection { () => (APIRoot apiRoot) => MyInfoModule(apiRoot), const [ ParameterReflection( - TypeReflection(APIRoot), 'apiRoot', false, true, null) + TypeReflection(APIRoot), 'apiRoot', false, true, null, null) ], null, null, @@ -109,9 +109,9 @@ class MyInfoModule$reflection extends ClassReflection { false, const [ ParameterReflection( - TypeReflection.tString, 'msg', false, true, null), - ParameterReflection( - TypeReflection(APIRequest), 'request', false, true, null) + TypeReflection.tString, 'msg', false, true, null, null), + ParameterReflection(TypeReflection(APIRequest), 'request', false, + true, null, null) ], null, null, diff --git a/test/bones_api_test_entities.dart b/test/bones_api_test_entities.dart index 402e780..bbe73cc 100644 --- a/test/bones_api_test_entities.dart +++ b/test/bones_api_test_entities.dart @@ -9,6 +9,9 @@ part 'bones_api_test_entities.reflection.g.dart'; final addressEntityHandler = GenericEntityHandler
( instantiatorFromMap: (m) => Address.fromMap(m)); +final roleEntityHandler = + GenericEntityHandler(instantiatorFromMap: (m) => Role.fromMap(m)); + final userEntityHandler = GenericEntityHandler(instantiatorFromMap: (m) => User.fromMap(m)); @@ -21,6 +24,11 @@ class AddressAPIRepository extends APIRepository
{ } } +class RoleAPIRepository extends APIRepository { + RoleAPIRepository(EntityRepositoryProvider provider) + : super(provider: provider); +} + class UserAPIRepository extends APIRepository { UserAPIRepository(EntityRepositoryProvider provider) : super(provider: provider); @@ -44,17 +52,23 @@ class User extends Entity { Address address; + List roles; + DateTime creationTime; - User(this.email, this.password, this.address, + User(this.email, this.password, this.address, this.roles, {this.id, DateTime? creationTime}) : creationTime = creationTime ?? DateTime.now(); - User.empty() : this('', '', Address.empty()); + User.empty() : this('', '', Address.empty(), []); - static FutureOr fromMap(Map map) => - User(map['email'], map['password'], map['address']!, - id: map['id'], creationTime: map['creationTime']); + static FutureOr fromMap(Map map) => User( + map.getAsString('email')!, + map.getAsString('password')!, + map.get
('address')!, + map.getAsList('roles', def: [])!, + id: map['id'], + creationTime: map['creationTime']); @override bool operator ==(Object other) => @@ -68,8 +82,14 @@ class User extends Entity { String get idFieldName => 'id'; @override - List get fieldsNames => - const ['id', 'email', 'password', 'address', 'creationTime']; + List get fieldsNames => const [ + 'id', + 'email', + 'password', + 'address', + 'roles', + 'creationTime' + ]; @override V? getField(String key) { @@ -82,6 +102,8 @@ class User extends Entity { return password as V?; case 'address': return address as V?; + case 'roles': + return roles as V?; case 'creationTime': return creationTime as V?; default: @@ -90,18 +112,20 @@ class User extends Entity { } @override - Type? getFieldType(String key) { + TypeInfo? getFieldType(String key) { switch (key) { case 'id': - return int; + return TypeInfo.tInt; case 'email': - return String; + return TypeInfo.tString; case 'password': - return String; + return TypeInfo.tString; case 'address': - return Address; + return TypeInfo(Address); + case 'roles': + return TypeInfo(List, [Role]); case 'creationTime': - return DateTime; + return TypeInfo(DateTime); default: return null; } @@ -112,7 +136,7 @@ class User extends Entity { switch (key) { case 'id': { - id = value as int; + id = value as int?; break; } case 'email': @@ -130,6 +154,11 @@ class User extends Entity { address = value as Address; break; } + case 'roles': + { + roles = value as List; + break; + } case 'creationTime': { creationTime = value as DateTime; @@ -146,6 +175,7 @@ class User extends Entity { 'email': email, 'password': password, 'address': address.toJson(), + 'roles': roles.map((e) => e.toJson()).toList(), 'creationTime': creationTime.millisecondsSinceEpoch, }; } @@ -173,8 +203,9 @@ class Address extends Entity { Address.empty() : this('', '', '', 0); Address.fromMap(Map map) - : this(map['state'], map['city'], map['street'], map['number'], - id: map['id']); + : this(map.getAsString('state')!, map.getAsString('city')!, + map.getAsString('street')!, map.getAsInt('number')!, + id: map.getAsInt('id')); @override bool operator ==(Object other) => @@ -221,18 +252,18 @@ class Address extends Entity { } @override - Type? getFieldType(String key) { + TypeInfo? getFieldType(String key) { switch (key) { case 'id': - return int; + return TypeInfo.tInt; case 'state': - return String; + return TypeInfo.tString; case 'city': - return String; + return TypeInfo.tString; case 'street': - return String; + return TypeInfo.tString; case 'number': - return int; + return TypeInfo.tInt; default: return null; } @@ -243,7 +274,7 @@ class Address extends Entity { switch (key) { case 'id': { - id = value as int; + id = value as int?; break; } case 'state': @@ -280,3 +311,96 @@ class Address extends Entity { 'number': number, }; } + +@EnableReflection() +class Role extends Entity { + int? id; + + String type; + + bool enabled; + + Role(this.type, {this.id, this.enabled = true}); + + Role.empty() : this(''); + + Role.fromMap(Map map) + : this(map.getAsString('type')!, + enabled: map.getAsBool('enabled', false)!, id: map.getAsInt('id')); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Role && + runtimeType == other.runtimeType && + id == other.id && + type == other.type && + enabled == other.enabled; + + @override + int get hashCode => id.hashCode ^ type.hashCode ^ enabled.hashCode; + + @override + String get idFieldName => 'id'; + + @override + List get fieldsNames => const ['id', 'type', 'enabled']; + + @override + V? getField(String key) { + switch (key) { + case 'id': + return id as V?; + case 'type': + return type as V?; + case 'enabled': + return enabled as V?; + default: + return null; + } + } + + @override + TypeInfo? getFieldType(String key) { + switch (key) { + case 'id': + return TypeInfo.tInt; + case 'type': + return TypeInfo.tString; + case 'enabled': + return TypeInfo.tBool; + default: + return null; + } + } + + @override + void setField(String key, V? value) { + switch (key) { + case 'id': + { + id = value as int?; + break; + } + case 'type': + { + type = value as String; + break; + } + case 'enabled': + { + enabled = value as bool; + break; + } + default: + return; + } + } + + @override + Map toJson() => { + if (id != null) 'id': id, + 'type': type, + 'enabled': enabled, + }; +} diff --git a/test/bones_api_test_entities.reflection.g.dart b/test/bones_api_test_entities.reflection.g.dart index a175410..ff547f9 100644 --- a/test/bones_api_test_entities.reflection.g.dart +++ b/test/bones_api_test_entities.reflection.g.dart @@ -1,6 +1,6 @@ // // GENERATED CODE - DO NOT MODIFY BY HAND! -// BUILDER: reflection_factory/1.0.10 +// BUILDER: reflection_factory/1.0.11 // BUILD COMMAND: dart run build_runner build // @@ -51,18 +51,18 @@ class Address$reflection extends ClassReflection
{ Address(state, city, street, number, id: id), const [ ParameterReflection( - TypeReflection.tString, 'state', false, true, null), + TypeReflection.tString, 'state', false, true, null, null), ParameterReflection( - TypeReflection.tString, 'city', false, true, null), + TypeReflection.tString, 'city', false, true, null, null), ParameterReflection( - TypeReflection.tString, 'street', false, true, null), + TypeReflection.tString, 'street', false, true, null, null), ParameterReflection( - TypeReflection.tInt, 'number', false, true, null) + TypeReflection.tInt, 'number', false, true, null, null) ], null, const { 'id': ParameterReflection( - TypeReflection.tInt, 'id', true, false, null) + TypeReflection.tInt, 'id', true, false, null, null) }, null); case 'empty': @@ -74,8 +74,8 @@ class Address$reflection extends ClassReflection
{ 'fromMap', () => (Map map) => Address.fromMap(map), const [ - ParameterReflection( - TypeReflection.tMapStringDynamic, 'map', false, true, null) + ParameterReflection(TypeReflection.tMapStringDynamic, 'map', + false, true, null, null) ], null, null, @@ -246,7 +246,7 @@ class Address$reflection extends ClassReflection
{ false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null) + TypeReflection.tString, 'key', false, true, null, null) ], null, null, @@ -255,14 +255,14 @@ class Address$reflection extends ClassReflection
{ return MethodReflection( this, 'getFieldType', - TypeReflection(Type), + TypeReflection(TypeInfo), true, (o) => o!.getFieldType, obj, false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null) + TypeReflection.tString, 'key', false, true, null, null) ], null, null, @@ -278,9 +278,9 @@ class Address$reflection extends ClassReflection
{ false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null), + TypeReflection.tString, 'key', false, true, null, null), ParameterReflection( - TypeReflection.tObject, 'value', true, true, null) + TypeReflection.tObject, 'value', true, true, null, null) ], null, null, @@ -312,6 +312,279 @@ class Address$reflection extends ClassReflection
{ } } +class Role$reflection extends ClassReflection { + Role$reflection([Role? object]) : super(Role, object); + + bool _registered = false; + @override + void register() { + if (!_registered) { + _registered = true; + super.register(); + } + } + + @override + Version get languageVersion => Version.parse('2.13.0'); + + @override + Role$reflection withObject([Role? obj]) => Role$reflection(obj); + + @override + bool get hasDefaultConstructor => false; + @override + Role? createInstanceWithDefaultConstructor() => null; + + @override + bool get hasEmptyConstructor => true; + @override + Role? createInstanceWithEmptyConstructor() => Role.empty(); + + @override + List get constructorsNames => const ['', 'empty', 'fromMap']; + + @override + ConstructorReflection? constructor(String constructorName) { + var lc = constructorName.trim().toLowerCase(); + + switch (lc) { + case '': + return ConstructorReflection( + this, + '', + () => (String type, {int? id, bool enabled = true}) => + Role(type, id: id, enabled: enabled), + const [ + ParameterReflection( + TypeReflection.tString, 'type', false, true, null, null) + ], + null, + const { + 'enabled': ParameterReflection( + TypeReflection.tBool, 'enabled', false, false, true, null), + 'id': ParameterReflection( + TypeReflection.tInt, 'id', true, false, null, null) + }, + null); + case 'empty': + return ConstructorReflection( + this, 'empty', () => () => Role.empty(), null, null, null, null); + case 'frommap': + return ConstructorReflection( + this, + 'fromMap', + () => (Map map) => Role.fromMap(map), + const [ + ParameterReflection(TypeReflection.tMapStringDynamic, 'map', + false, true, null, null) + ], + null, + null, + null); + default: + return null; + } + } + + @override + List get classAnnotations => List.unmodifiable([]); + + @override + List get fieldsNames => const [ + 'enabled', + 'fieldsNames', + 'hashCode', + 'id', + 'idFieldName', + 'type' + ]; + + @override + FieldReflection? field(String fieldName, [Role? obj]) { + obj ??= object!; + + var lc = fieldName.trim().toLowerCase(); + + switch (lc) { + case 'id': + return FieldReflection( + this, + TypeReflection.tInt, + 'id', + true, + (o) => () => o!.id as T, + (o) => (T? v) => o!.id = v as int?, + obj, + false, + false, + null, + ); + case 'type': + return FieldReflection( + this, + TypeReflection.tString, + 'type', + false, + (o) => () => o!.type as T, + (o) => (T? v) => o!.type = v as String, + obj, + false, + false, + null, + ); + case 'enabled': + return FieldReflection( + this, + TypeReflection.tBool, + 'enabled', + false, + (o) => () => o!.enabled as T, + (o) => (T? v) => o!.enabled = v as bool, + obj, + false, + false, + null, + ); + case 'hashcode': + return FieldReflection( + this, + TypeReflection.tInt, + 'hashCode', + false, + (o) => () => o!.hashCode as T, + null, + obj, + false, + false, + null, + ); + case 'idfieldname': + return FieldReflection( + this, + TypeReflection.tString, + 'idFieldName', + false, + (o) => () => o!.idFieldName as T, + null, + obj, + false, + false, + null, + ); + case 'fieldsnames': + return FieldReflection( + this, + TypeReflection.tListString, + 'fieldsNames', + false, + (o) => () => o!.fieldsNames as T, + null, + obj, + false, + false, + null, + ); + default: + return null; + } + } + + @override + List get staticFieldsNames => const []; + + @override + FieldReflection? staticField(String fieldName) { + return null; + } + + @override + List get methodsNames => + const ['getField', 'getFieldType', 'setField', 'toJson']; + + @override + MethodReflection? method(String methodName, [Role? obj]) { + obj ??= object; + + var lc = methodName.trim().toLowerCase(); + + switch (lc) { + case 'getfield': + return MethodReflection( + this, + 'getField', + TypeReflection.tObject, + true, + (o) => o!.getField, + obj, + false, + const [ + ParameterReflection( + TypeReflection.tString, 'key', false, true, null, null) + ], + null, + null, + [override]); + case 'getfieldtype': + return MethodReflection( + this, + 'getFieldType', + TypeReflection(TypeInfo), + true, + (o) => o!.getFieldType, + obj, + false, + const [ + ParameterReflection( + TypeReflection.tString, 'key', false, true, null, null) + ], + null, + null, + [override]); + case 'setfield': + return MethodReflection( + this, + 'setField', + null, + false, + (o) => o!.setField, + obj, + false, + const [ + ParameterReflection( + TypeReflection.tString, 'key', false, true, null, null), + ParameterReflection( + TypeReflection.tObject, 'value', true, true, null, null) + ], + null, + null, + [override]); + case 'tojson': + return MethodReflection( + this, + 'toJson', + TypeReflection.tMapStringDynamic, + false, + (o) => o!.toJson, + obj, + false, + null, + null, + null, + [override]); + default: + return null; + } + } + + @override + List get staticMethodsNames => const []; + + @override + MethodReflection? staticMethod(String methodName) { + return null; + } +} + class User$reflection extends ClassReflection { User$reflection([User? object]) : super(User, object); @@ -353,23 +626,25 @@ class User$reflection extends ClassReflection { this, '', () => (String email, String password, Address address, - {int? id, DateTime? creationTime}) => - User(email, password, address, + List roles, {int? id, DateTime? creationTime}) => + User(email, password, address, roles, id: id, creationTime: creationTime), const [ ParameterReflection( - TypeReflection.tString, 'email', false, true, null), + TypeReflection.tString, 'email', false, true, null, null), ParameterReflection( - TypeReflection.tString, 'password', false, true, null), + TypeReflection.tString, 'password', false, true, null, null), ParameterReflection( - TypeReflection(Address), 'address', false, true, null) + TypeReflection(Address), 'address', false, true, null, null), + ParameterReflection(TypeReflection(List, [Role]), 'roles', false, + true, null, null) ], null, const { - 'creationTime': ParameterReflection( - TypeReflection(DateTime), 'creationTime', true, false, null), + 'creationTime': ParameterReflection(TypeReflection(DateTime), + 'creationTime', true, false, null, null), 'id': ParameterReflection( - TypeReflection.tInt, 'id', true, false, null) + TypeReflection.tInt, 'id', true, false, null, null) }, null); case 'empty': @@ -392,7 +667,8 @@ class User$reflection extends ClassReflection { 'hashCode', 'id', 'idFieldName', - 'password' + 'password', + 'roles' ]; @override @@ -454,6 +730,19 @@ class User$reflection extends ClassReflection { false, null, ); + case 'roles': + return FieldReflection( + this, + TypeReflection(List, [Role]), + 'roles', + false, + (o) => () => o!.roles as T, + (o) => (T? v) => o!.roles = v as List, + obj, + false, + false, + null, + ); case 'creationtime': return FieldReflection( this, @@ -541,7 +830,7 @@ class User$reflection extends ClassReflection { false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null) + TypeReflection.tString, 'key', false, true, null, null) ], null, null, @@ -550,14 +839,14 @@ class User$reflection extends ClassReflection { return MethodReflection( this, 'getFieldType', - TypeReflection(Type), + TypeReflection(TypeInfo), true, (o) => o!.getFieldType, obj, false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null) + TypeReflection.tString, 'key', false, true, null, null) ], null, null, @@ -573,9 +862,9 @@ class User$reflection extends ClassReflection { false, const [ ParameterReflection( - TypeReflection.tString, 'key', false, true, null), + TypeReflection.tString, 'key', false, true, null, null), ParameterReflection( - TypeReflection.tObject, 'value', true, true, null) + TypeReflection.tObject, 'value', true, true, null, null) ], null, null, @@ -616,8 +905,8 @@ class User$reflection extends ClassReflection { null, true, const [ - ParameterReflection( - TypeReflection.tMapStringDynamic, 'map', false, true, null) + ParameterReflection(TypeReflection.tMapStringDynamic, 'map', + false, true, null, null) ], null, null, @@ -636,6 +925,14 @@ extension Address$reflectionExtension on Address { String toJsonEncoded() => reflection.toJsonEncoded(); } +extension Role$reflectionExtension on Role { + /// Returns a [ClassReflection] for type [Role]. (Generated by [ReflectionFactory]) + ClassReflection get reflection => Role$reflection(this); + + /// Returns an encoded JSON [String] for type [Role]. (Generated by [ReflectionFactory]) + String toJsonEncoded() => reflection.toJsonEncoded(); +} + extension User$reflectionExtension on User { /// Returns a [ClassReflection] for type [User]. (Generated by [ReflectionFactory]) ClassReflection get reflection => User$reflection(this); diff --git a/test/bones_api_utils_test.dart b/test/bones_api_utils_test.dart index b231202..77391cc 100644 --- a/test/bones_api_utils_test.dart +++ b/test/bones_api_utils_test.dart @@ -214,18 +214,72 @@ void main() { group('TimedMap', () { test('basic', () async { + var m = TimedMap(Duration(seconds: 1), {'a': 1, 'b': 2}); + + expect(m.isEmpty, isFalse); + expect(m.isNotEmpty, isTrue); + expect(m.length, equals(2)); + expect(m.keys, equals(['a', 'b'])); + + expect(m['a'], equals(1)); + expect(m['b'], equals(2)); + + expect(m.cast().toString(), equals('{a: 1, b: 2}')); + + m['a'] = 10; + expect(m['a'], equals(10)); + + m.update('b', (v) => v * 2); + m.updateTimed('c', (v) => v * 2, ifAbsent: () => 3); + + m.putIfAbsent('b', () => 200); + m.putIfAbsentChecked('c', () => 300); + + expect(m.toString(), equals('{a: 10, b: 4, c: 3}')); + + m.updateAll((key, value) => value * 2); + + expect(m.toString(), equals('{a: 20, b: 8, c: 6}')); + + m.removeWhere((key, value) => value > 10); + + expect(m.toString(), equals('{b: 8, c: 6}')); + + m.clear(); + expect(m.isEmpty, isTrue); + expect(m.keys, equals([])); + + await m.putIfAbsentCheckedAsync('d', () => Future.value(4)); + expect(m.keys, equals(['d'])); + }); + + test('timed', () async { var m = TimedMap(Duration(seconds: 1)); expect(m.isEmpty, isTrue); expect(m.isNotEmpty, isFalse); expect(m.length, equals(0)); + expect(m.keys, equals([])); + expect(m.values, equals([])); + expect(m.entries, equals([])); + m.addAll({'a': 1, 'b': 2}); expect(m.isEmpty, isFalse); expect(m.isNotEmpty, isTrue); expect(m.length, equals(2)); + expect(m.keys, equals(['a', 'b'])); + expect(m.values, equals([1, 2])); + expect(m.entries.map((e) => e.key), equals(['a', 'b'])); + expect(m.entries.map((e) => e.value), equals([1, 2])); + + expect(m.keysChecked(), equals(['a', 'b'])); + expect(m.valuesChecked(), equals([1, 2])); + expect(m.entriesChecked().map((e) => e.key), equals(['a', 'b'])); + expect(m.entriesChecked().map((e) => e.value), equals([1, 2])); + expect(m['a'], equals(1)); expect(m['b'], equals(2)); @@ -253,6 +307,8 @@ void main() { await Future.delayed(Duration(milliseconds: 1100)); + expect(m.getElapsedTime('a')!.inMilliseconds > 1000, isTrue); + expect(m.getChecked('a'), isNull); expect(m.getChecked('b'), isNull); expect(m.getChecked('c'), isNull);