From 8a95a798c33a66bce636bc05f640517933f3795a Mon Sep 17 00:00:00 2001 From: gmpassos Date: Tue, 7 Sep 2021 21:04:58 -0300 Subject: [PATCH] v1.0.7 - Added `APIPayload.payloadFileExtension`. - Added `ConditionEncoder`, `ConditionSQLEncoder`. - Improved Data & Entity framework: - Added `SQLDatabaseAdapter` and `PostgreAdapter`. - Added `DataRepositorySQL`. - Added DB Adapter for PostgreSQL. - APIServer: - Better auto MIME Type resolution. - Now API methods can return `FutureOr`. - mime: ^1.0.0 --- CHANGELOG.md | 13 + lib/bones_api.dart | 5 +- lib/bones_api_adapter_postgre.dart | 4 + lib/src/bones_api_base.dart | 15 +- lib/src/bones_api_condition.dart | 2 +- lib/src/bones_api_condition_encoder.dart | 220 ++++++++++++++ lib/src/bones_api_condition_sql.dart | 141 +++++++++ lib/src/bones_api_data.dart | 308 +++++++++++++++++--- lib/src/bones_api_data_adapter.dart | 254 ++++++++++++++++ lib/src/bones_api_data_adapter_postgre.dart | 84 ++++++ lib/src/bones_api_data_sql.dart | 91 ++++++ lib/src/bones_api_extension.dart | 19 +- lib/src/bones_api_hotreload_vm.dart | 6 +- lib/src/bones_api_mixin.dart | 206 +++++++++++++ lib/src/bones_api_repository.dart | 34 ++- lib/src/bones_api_server.dart | 29 +- pubspec.yaml | 7 +- test/bones_api_data_test.dart | 49 +++- 18 files changed, 1418 insertions(+), 69 deletions(-) create mode 100644 lib/bones_api_adapter_postgre.dart create mode 100644 lib/src/bones_api_condition_encoder.dart create mode 100644 lib/src/bones_api_condition_sql.dart create mode 100644 lib/src/bones_api_data_adapter.dart create mode 100644 lib/src/bones_api_data_adapter_postgre.dart create mode 100644 lib/src/bones_api_data_sql.dart create mode 100644 lib/src/bones_api_mixin.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea29fc..3227889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.0.7 + +- Added `APIPayload.payloadFileExtension`. +- Added `ConditionEncoder`, `ConditionSQLEncoder`. +- Improved Data & Entity framework: + - Added `SQLDatabaseAdapter` and `PostgreAdapter`. + - Added `DataRepositorySQL`. +- Added DB Adapter for PostgreSQL. +- APIServer: + - Better auto MIME Type resolution. +- Now API methods can return `FutureOr`. +- mime: ^1.0.0 + ## 1.0.6 - CLI Hot Reload fixed: diff --git a/lib/bones_api.dart b/lib/bones_api.dart index 862c675..dc8ab1e 100644 --- a/lib/bones_api.dart +++ b/lib/bones_api.dart @@ -15,7 +15,10 @@ library bones_api; export 'src/bones_api_base.dart'; -export 'src/bones_api_data.dart'; export 'src/bones_api_condition.dart'; +export 'src/bones_api_condition_sql.dart'; +export 'src/bones_api_data.dart'; +export 'src/bones_api_data_adapter.dart'; +export 'src/bones_api_data_sql.dart'; export 'src/bones_api_extension.dart'; export 'src/bones_api_repository.dart'; diff --git a/lib/bones_api_adapter_postgre.dart b/lib/bones_api_adapter_postgre.dart new file mode 100644 index 0000000..629aef0 --- /dev/null +++ b/lib/bones_api_adapter_postgre.dart @@ -0,0 +1,4 @@ +/// Bones_API DB Adapter for PostgreSQL. +library bones_api_data_adapter_postgre; + +export 'src/bones_api_data_adapter_postgre.dart'; diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 596866b..d007699 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -420,6 +420,9 @@ abstract class APIPayload { /// The payload MIME Type. String? get payloadMimeType; + /// The payload usual file name extension. + String? get payloadFileExtension; + /// Returns `true` if [payload] is not `null`. bool get hasPayload => payload != null; @@ -469,13 +472,18 @@ class APIRequest extends APIPayload { @override String? payloadMimeType; + /// The payload usual file name extension. + @override + String? payloadFileExtension; + late final List _pathParts; APIRequest(this.method, this.path, {Map? parameters, Map? headers, this.payload, - this.payloadMimeType}) + this.payloadMimeType, + this.payloadFileExtension}) : parameters = parameters ?? {}, headers = headers ?? {}, _pathParts = _buildPathParts(path); @@ -768,6 +776,10 @@ class APIResponse extends APIPayload { @override String? payloadMimeType; + /// The payload usual file name extension. + @override + String? payloadFileExtension; + /// The response error. final dynamic error; @@ -775,6 +787,7 @@ class APIResponse extends APIPayload { {this.headers = const {}, this.payload, this.payloadMimeType, + this.payloadFileExtension, this.error}); /// Creates a response of status `OK`. diff --git a/lib/src/bones_api_condition.dart b/lib/src/bones_api_condition.dart index 76422da..d01c36e 100644 --- a/lib/src/bones_api_condition.dart +++ b/lib/src/bones_api_condition.dart @@ -545,7 +545,7 @@ abstract class KeyCondition extends Condition { Object? value; if (key is ConditionKeyField) { - var objDataHandler = dataHandler?.getDataHandler(obj); + var objDataHandler = dataHandler?.getDataHandler(obj: obj); value = fieldAccessor.getField(obj, key.name, dataHandler: objDataHandler); } else if (key is ConditionKeyIndex) { diff --git a/lib/src/bones_api_condition_encoder.dart b/lib/src/bones_api_condition_encoder.dart new file mode 100644 index 0000000..7d0d0a8 --- /dev/null +++ b/lib/src/bones_api_condition_encoder.dart @@ -0,0 +1,220 @@ +import 'bones_api_condition.dart'; + +abstract class ConditionEncoder { + ConditionEncoder(); + + String encode( + Condition condition, Map parametersPlaceholders, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) { + var s = StringBuffer(); + + var rootIsGroup = condition is GroupCondition; + + if (!rootIsGroup) { + s.write(groupOpener); + } + + encodeCondition(condition, parametersPlaceholders, s, parameters, + positionalParameters, namedParameters); + + if (!rootIsGroup) { + s.write(groupCloser); + } + + return s.toString(); + } + + StringBuffer encodeCondition( + Condition c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + if (c is KeyCondition) { + return encodeKeyCondition( + c, p, s, parameters, positionalParameters, namedParameters); + } else if (c is GroupCondition) { + return encodeGroupCondition( + c, p, s, parameters, positionalParameters, namedParameters); + } else if (c is IDCondition) { + return encodeIDCondition( + c, p, s, parameters, positionalParameters, namedParameters); + } else { + throw ConditionEncodingError("$c"); + } + } + + StringBuffer encodeIDCondition( + IDCondition c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters); + + StringBuffer encodeGroupCondition( + GroupCondition c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + if (c is GroupConditionAND) { + return encodeGroupConditionAND( + c, p, s, parameters, positionalParameters, namedParameters); + } else if (c is GroupConditionOR) { + return encodeGroupConditionOR( + c, p, s, parameters, positionalParameters, namedParameters); + } else { + throw ConditionEncodingError("$c"); + } + } + + String get groupOpener; + + String get groupCloser; + + String get groupOperatorAND; + + String get groupOperatorOR; + + StringBuffer encodeGroupConditionAND( + GroupConditionAND c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters, + ) { + s ??= StringBuffer(); + + var conditions = c.conditions; + var length = conditions.length; + + if (length == 0) { + return s; + } + + s.write(groupOpener); + encodeCondition(conditions.first, p, s, parameters, positionalParameters, + namedParameters); + + for (var i = 1; i < length; ++i) { + s.write(groupOperatorAND); + + var c2 = conditions[i]; + encodeCondition( + c2, p, s, parameters, positionalParameters, namedParameters); + } + + s.write(groupCloser); + + return s; + } + + StringBuffer encodeGroupConditionOR( + GroupConditionOR c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + var conditions = c.conditions; + var length = conditions.length; + + if (length == 0) { + return s; + } + + s.write(groupOpener); + + encodeCondition(conditions.first, p, s, parameters, positionalParameters, + namedParameters); + + for (var i = 1; i < length; ++i) { + s.write(groupOperatorOR); + + var c2 = conditions[i]; + encodeCondition( + c2, p, s, parameters, positionalParameters, namedParameters); + } + + s.write(groupCloser); + + return s; + } + + StringBuffer encodeKeyCondition( + KeyCondition c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + if (c is KeyConditionEQ) { + return encodeKeyConditionEQ( + c, p, s, parameters, positionalParameters, namedParameters); + } else if (c is KeyConditionNotEQ) { + return encodeKeyConditionNotEQ( + c, p, s, parameters, positionalParameters, namedParameters); + } else { + throw ConditionEncodingError("$c"); + } + } + + StringBuffer encodeKeyConditionEQ( + KeyConditionEQ c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters); + + StringBuffer encodeKeyConditionNotEQ( + KeyConditionNotEQ c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters); + + String resolveParameterSQL( + String valueKey, + ConditionParameter value, + Map p, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + p.putIfAbsent( + valueKey, + () => value.getValue( + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters)); + + var placeholder = parameterPlaceholder(valueKey); + return placeholder; + } + + String parameterPlaceholder(String parameterKey); +} + +class ConditionEncodingError extends Error { + final String message; + + ConditionEncodingError(this.message); + + @override + String toString() => "Encoding error: $message"; +} diff --git a/lib/src/bones_api_condition_sql.dart b/lib/src/bones_api_condition_sql.dart new file mode 100644 index 0000000..e39aa9b --- /dev/null +++ b/lib/src/bones_api_condition_sql.dart @@ -0,0 +1,141 @@ +import 'bones_api_condition.dart'; +import 'bones_api_condition_encoder.dart'; + +class ConditionSQLEncoder extends ConditionEncoder { + ConditionSQLEncoder(); + + @override + String get groupOpener => '('; + + @override + String get groupCloser => ')'; + + @override + String get groupOperatorAND => 'AND'; + + @override + String get groupOperatorOR => 'OR'; + + @override + String parameterPlaceholder(String parameterKey) => '@$parameterKey'; + + @override + StringBuffer encodeIDCondition( + IDCondition c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + s.write(' id = '); + + var valueSQL = valueToSQL( + c.idValue, p, parameters, positionalParameters, namedParameters); + + s.write(valueSQL); + s.write(' '); + + return s; + } + + @override + StringBuffer encodeKeyConditionEQ( + KeyConditionEQ c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + var keySQL = keyToSQL(c); + + s.write(' '); + s.write(keySQL); + + s.write(" = "); + + var valueSQL = valueToSQL( + c.value, p, parameters, positionalParameters, namedParameters); + + s.write(valueSQL); + s.write(' '); + + return s; + } + + @override + StringBuffer encodeKeyConditionNotEQ( + KeyConditionNotEQ c, + Map p, + StringBuffer? s, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + s ??= StringBuffer(); + + var keySQL = keyToSQL(c); + + s.write(' '); + s.write(keySQL); + + s.write(" != "); + + var valueSQL = valueToSQL( + c.value, p, parameters, positionalParameters, namedParameters); + + s.write(valueSQL); + s.write(' '); + + return s; + } + + String keyToSQL(KeyCondition c) { + if (c.keys.length == 1) { + var key = c.keys[0]; + + if (key is ConditionKeyField) { + return '"${key.name}"'; + } + } + + throw ConditionEncodingError("Key: $c"); + } + + String valueToSQL(dynamic value, Map p, Object? parameters, + List? positionalParameters, Map? namedParameters) { + if (value is ConditionParameter) { + return conditionParameterToSQL( + value, p, parameters, positionalParameters, namedParameters); + } else if (value is num || value is bool) { + return value.toString(); + } else { + var valueStr = '$value'; + valueStr = valueStr.replaceAll("'", r"\'"); + return "'$valueStr'"; + } + } + + String conditionParameterToSQL( + ConditionParameter parameter, + Map p, + Object? parameters, + List? positionalParameters, + Map? namedParameters) { + if (parameter.hasKey) { + return resolveParameterSQL(parameter.key!, parameter, p, parameters, + positionalParameters, namedParameters); + } else { + var contextKey = parameter.contextKey; + + if (contextKey != null) { + return resolveParameterSQL(contextKey, parameter, p, parameters, + positionalParameters, namedParameters); + } + + throw ConditionEncodingError('$parameter'); + } + } +} diff --git a/lib/src/bones_api_data.dart b/lib/src/bones_api_data.dart index a4f557c..3a9bccf 100644 --- a/lib/src/bones_api_data.dart +++ b/lib/src/bones_api_data.dart @@ -1,7 +1,10 @@ +import 'dart:async'; import 'dart:convert' as dart_convert; +import 'package:async_extension/async_extension.dart'; +import 'package:bones_api/src/bones_api_mixin.dart'; import 'package:collection/collection.dart'; -import 'package:reflection_factory/builder.dart'; +import 'package:reflection_factory/reflection_factory.dart'; import 'bones_api_condition.dart'; @@ -18,6 +21,8 @@ abstract class DataEntity { V? getField(String key); + Type? getFieldType(String key); + void setField(String key, V? value); Map toJson(); @@ -36,14 +41,21 @@ class DataHandlerProvider { _dataHandlers[dataHandler.type] = dataHandler; } - DataHandler? getDataHandler([O? o]) => - _getDataHandlerImpl(o) ?? _globalProvider._getDataHandlerImpl(o); + DataHandler? getDataHandler({O? obj, Type? type}) => + _getDataHandlerImpl(obj: obj, type: type) ?? + _globalProvider._getDataHandlerImpl(obj: obj, type: type); - DataHandler? _getDataHandlerImpl([O? o]) { + DataHandler? _getDataHandlerImpl({O? obj, Type? type}) { var dataHandler = _dataHandlers[O]; - if (dataHandler == null && o != null) { - dataHandler = _dataHandlers[o.runtimeType]; + + if (dataHandler == null && obj != null) { + dataHandler = _dataHandlers[obj.runtimeType]; + } + + if (dataHandler == null && type != null) { + dataHandler = _dataHandlers[type]; } + return dataHandler as DataHandler?; } } @@ -64,22 +76,27 @@ abstract class DataHandler { static bool isValidType([Type? type]) { type ??= T; - return type != Object && - type != dynamic && - type != String && - type != int && - type != double && - type != num && - type != bool; + 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; } - DataHandler? getDataHandler([T? o]) { + DataHandler? getDataHandler({T? obj, Type? type}) { if (T == O && isValidType()) { return this as DataHandler; - } else if (o != null && o.runtimeType == O && isValidType()) { + } else if (obj != null && obj.runtimeType == O && isValidType()) { + return this as DataHandler; + } else if (type != null && type == O && isValidType()) { return this as DataHandler; } else { - return provider.getDataHandler(o); + return provider.getDataHandler(obj: obj, type: type); } } @@ -89,10 +106,26 @@ abstract class DataHandler { List fieldsNames([O? o]); + Type? getFieldType(O o, String key); + V? getField(O o, String key); + Map getFields(O o) { + return Map.fromEntries(fieldsNames(o) + .map((key) => MapEntry(key, getField(o, key)))); + } + void setField(O o, String key, V? value); + bool trySetField(O o, String key, V? value) { + try { + setField(o, key, value); + return true; + } catch (e) { + return false; + } + } + JsonReviver? jsonReviver; O decodeObjectJson(String json) => @@ -117,12 +150,111 @@ abstract class DataHandler { String encodeJson(dynamic o) => dart_convert.json.encode(o, toEncodable: jsonToEncodable); + + FutureOr createFromMap(Map fields); + + FutureOr setFieldsFromMap(O o, Map fields) async { + for (var f in fieldsNames(o)) { + var val = getFieldFromMap(fields, f); + await setFieldValueDynamic(o, f, val); + } + return o; + } + + 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 { + var valRepo = getDataRepository(type: fieldType); + var valDynamicRet = valRepo?.selectByID(value); + + return valDynamicRet.resolveMapped((valDynamic) { + valDynamic ??= value; + setField(o, key, valDynamic); + return valDynamic; + }); + } + } + + static final RegExp _regexpNonWord = RegExp(r'\W'); + + V? getFieldFromMap(Map map, String key) { + var v = map[key]; + if (v != null) return v; + + var keyLC = key.toLowerCase(); + v = map[keyLC]; + if (v != null) return v; + + var keyLC2 = keyLC.replaceAll(_regexpNonWord, ''); + + for (var k in map.keys) { + var kLC = k.toLowerCase(); + + if (keyLC == kLC) { + var v = map[k]; + return v; + } + + var kLC2 = kLC.replaceAll(_regexpNonWord, ''); + + if (keyLC2 == kLC2) { + var v = map[k]; + return v; + } + } + + return null; + } + + final Set _knownDataRepositoryProviders = + {}; + + void notifyKnownDataRepositoryProvider(DataRepositoryProvider provider) { + _knownDataRepositoryProviders.add(provider); + } + + DataRepository? getDataRepository({T? obj, Type? type}) { + for (var provider in _knownDataRepositoryProviders) { + var repository = provider.getDataRepository(obj: obj, type: type); + if (repository != null) { + return repository; + } + } + return null; + } } +typedef InstantiatorDefault = FutureOr Function(); + +typedef InstantiatorFromMap = FutureOr Function(Map); + class EntityDataHandler extends DataHandler { + final InstantiatorDefault? instantiatorDefault; + + final InstantiatorFromMap? instantiatorFromMap; + EntityDataHandler( - {Type? type, O? sampleEntity, DataHandlerProvider? provider}) + {this.instantiatorDefault, + this.instantiatorFromMap, + Type? type, + O? sampleEntity, + DataHandlerProvider? provider}) : super(provider, type: type ?? O) { + if (instantiatorDefault == null && instantiatorFromMap == null) { + throw ArgumentError( + "Null instantiators: `instantiatorDefault`, `instantiatorFromMap`"); + } + if (sampleEntity != null) { _populateFieldsNames(sampleEntity); } @@ -175,11 +307,27 @@ class EntityDataHandler extends DataHandler { return o.getField(key); } + @override + Type? getFieldType(O o, String key) { + _populateFieldsNames(o); + return o.getFieldType(key); + } + @override void setField(O o, String key, V? value) { _populateFieldsNames(o); return o.setField(key, value); } + + @override + FutureOr createFromMap(Map fields) { + if (instantiatorFromMap != null) { + return instantiatorFromMap!(fields); + } else { + var oRet = instantiatorDefault!(); + return oRet.resolveMapped((o) => setFieldsFromMap(o, fields)); + } + } } class ClassReflectionDataHandler extends DataHandler { @@ -201,12 +349,46 @@ class ClassReflectionDataHandler extends DataHandler { @override V? getField(O o, String key) => reflection.getField(key, o); + @override + Type? getFieldType(O o, String key) { + var field = reflection.field(key, o); + return field?.type.type; + } + @override void setField(O o, String key, V? value) => reflection.setField(key, value, o); + @override + bool trySetField(O o, String key, V? value) { + var field = reflection.field(key, o); + if (field == null) return false; + + if (value == null) { + if (field.nullable) { + field.set(value); + return true; + } else { + return false; + } + } + + if (field.type.type == value.runtimeType) { + field.set(value); + return true; + } else { + return false; + } + } + @override List fieldsNames([O? o]) => reflection.fieldsNames; + + @override + FutureOr createFromMap(Map fields) { + var o = reflection.createInstance()!; + return setFieldsFromMap(o, fields); + } } mixin DataFieldAccessor { @@ -262,16 +444,17 @@ abstract class DataAccessor { abstract class DataSource extends DataAccessor { DataSource(String name) : super(name); - O? selectByID(dynamic id) { - var ret = select(IDCondition(id)); - return ret.isNotEmpty ? ret.first : null; + FutureOr selectByID(dynamic id) { + return select(IDCondition(id)).resolveMapped((sel) { + return sel.isNotEmpty ? sel.first : null; + }); } - int length(); + FutureOr length(); final ConditionParseCache _parseCache = ConditionParseCache.get(); - Iterable selectByQuery(String query, + FutureOr> selectByQuery(String query, {Object? parameters, List? positionalParameters, Map? namedParameters}) { @@ -283,7 +466,7 @@ abstract class DataSource extends DataAccessor { namedParameters: namedParameters); } - Iterable select(EntityMatcher matcher, + FutureOr> select(EntityMatcher matcher, {Object? parameters, List? positionalParameters, Map? namedParameters}); @@ -305,24 +488,58 @@ class DataRepositoryProvider { final Map _dataRepositories = {}; - void _register(DataRepository dataRepository) { + void registerDataRepository(DataRepository dataRepository) { _dataRepositories[dataRepository.type] = dataRepository; } - DataRepository? getDataRepository([O? o]) => - _getDataRepositoryImpl(o) ?? - _globalProvider._getDataRepositoryImpl(o); + DataRepository? getDataRepository({O? obj, Type? type}) { + var dataRepository = _getDataRepositoryImpl(obj: obj, type: type); + if (dataRepository != null) { + return dataRepository; + } - DataRepository? _getDataRepositoryImpl([O? o]) { + if (!identical(this, _globalProvider)) { + return _globalProvider._getDataRepositoryImpl(obj: obj, type: type); + } + + return null; + } + + DataRepository? _getDataRepositoryImpl({O? obj, Type? type}) { var dataRepository = _dataRepositories[O]; - if (dataRepository == null && o != null) { - dataRepository = _dataRepositories[o.runtimeType]; + + if (dataRepository == null && obj != null) { + dataRepository = _dataRepositories[obj.runtimeType]; + } + + if (dataRepository == null && type != null) { + dataRepository = _dataRepositories[type]; } - return dataRepository as DataRepository?; + + if (dataRepository != null) { + return dataRepository as DataRepository; + } else { + for (var p in _knownDataRepositoryProviders) { + dataRepository = p.getDataRepository(obj: obj, type: type); + if (dataRepository != null) { + return dataRepository as DataRepository; + } + } + + return null; + } + } + + final Set _knownDataRepositoryProviders = + {}; + + void notifyKnownDataRepositoryProvider(DataRepositoryProvider provider) { + _knownDataRepositoryProviders.add(provider); } } abstract class DataRepository extends DataAccessor + with Initializable implements DataSource, DataStorage { final DataRepositoryProvider provider; @@ -339,22 +556,17 @@ abstract class DataRepository extends DataAccessor throw StateError('Invalid DataRepository type: $type ?? $O'); } - this.provider._register(this); - } - - bool _initialized = false; - - void ensureInitialized() { - if (_initialized) { - return; - } + this.provider.registerDataRepository(this); - _initialized = true; - - initialize(); + dataHandler.notifyKnownDataRepositoryProvider(this.provider); } - void initialize() {} + @override + FutureOr selectByID(dynamic id) { + return select(IDCondition(id)).resolveMapped((sel) { + return sel.isNotEmpty ? sel.first : null; + }); + } dynamic ensureStored(O o); @@ -364,7 +576,7 @@ abstract class DataRepository extends DataAccessor final ConditionParseCache _parseCache = ConditionParseCache.get(); @override - Iterable selectByQuery(String query, + FutureOr> selectByQuery(String query, {Object? parameters, List? positionalParameters, Map? namedParameters}) { @@ -448,9 +660,11 @@ abstract class IterableDataRepository extends DataRepository if (id == null) { return store(o); + } else { + ensureReferencesStored(o); } - return null; + return id; } @override @@ -461,7 +675,7 @@ abstract class IterableDataRepository extends DataRepository continue; } - var repository = provider.getDataRepository(value); + var repository = provider.getDataRepository(obj: value); if (repository == null) { continue; } diff --git a/lib/src/bones_api_data_adapter.dart b/lib/src/bones_api_data_adapter.dart new file mode 100644 index 0000000..874444a --- /dev/null +++ b/lib/src/bones_api_data_adapter.dart @@ -0,0 +1,254 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async_extension/async_extension.dart'; +import 'package:logging/logging.dart' as logging; + +import 'bones_api_condition.dart'; +import 'bones_api_condition_sql.dart'; +import 'bones_api_data.dart'; +import 'bones_api_mixin.dart'; + +final _log = logging.Logger('SQLDatabaseAdapter'); + +typedef PasswordProvider = FutureOr Function(String user); + +class SQL { + final String sql; + + final dynamic parameters; + + SQL(this.sql, this.parameters); + + @override + String toString() { + return 'SQL<< $sql >>( ${json.encode(parameters)} )'; + } +} + +abstract class SQLDatabaseAdapter + with Initializable, Pool + implements DataRepositoryProvider { + final int minConnections; + + final int maxConnections; + + final String dialect; + + final DataRepositoryProvider? parentRepositoryProvider; + + SQLDatabaseAdapter(this.minConnections, this.maxConnections, this.dialect, + {this.parentRepositoryProvider}); + + static final ConditionSQLEncoder _conditionSQLGenerator = + ConditionSQLEncoder(); + + @override + void initialize(); + + String generateLengthSQL(String table) { + throw UnimplementedError(); + } + + SQL generateSelectSQL(String table, EntityMatcher matcher, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) { + if (matcher is Condition) { + Map sqlParameters = {}; + + var conditionSQL = _conditionSQLGenerator.encode(matcher, sqlParameters, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); + + String sqlQuery; + if (conditionSQL.isNotEmpty) { + sqlQuery = 'SELECT * FROM $table WHERE $conditionSQL'; + } else { + sqlQuery = 'SELECT * FROM $table'; + } + + var sql = SQL(sqlQuery, sqlParameters); + + return sql; + } else { + throw StateError('$matcher'); + } + } + + String generateInsertSQL(String table, Map fields) { + throw UnimplementedError(); + } + + T? executeSQL(String sql) { + return null; + } + + FutureOr>> selectSQL(String table, SQL sql, + {Object? parameters, + List? positionalParameters, + Map? namedParameters, + Map Function(Map r)? mapper}) { + return catchFromPool().resolveMapped((connection) { + _log.log(logging.Level.INFO, 'selectSQL> $sql'); + + return doSelectSQL(table, sql, connection, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters) + .resolveMapped((entries) { + if (mapper != null) { + return entries.map(mapper); + } else { + return entries; + } + }); + }); + } + + FutureOr>> doSelectSQL( + String table, SQL sql, C connection, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}); + + FutureOr createConnection(); + + FutureOr isConnectionValid(C connection); + + FutureOr checkConnections() => removeInvalidElementsFromPool(); + + @override + FutureOr isPoolElementValid(C o) => isConnectionValid(o); + + @override + FutureOr checkPool() => + checkPoolSize(minConnections, maxConnections, 30000); + + @override + FutureOr createPoolElement() { + if (poolSize < maxConnections) { + return createConnection(); + } else { + return null; + } + } + + final Map _repositoriesAdapters = + {}; + + SQLRepositoryAdapter getRepositoryAdapter(String name, + {String? tableName, Type? type}) { + return _repositoriesAdapters.putIfAbsent( + name, + () => SQLRepositoryAdapter(this, name, + tableName: tableName, type: type)) as SQLRepositoryAdapter; + } + + final Map _dataRepositories = {}; + + @override + void registerDataRepository(DataRepository dataRepository) { + _dataRepositories[dataRepository.type] = dataRepository; + } + + @override + DataRepository? getDataRepository({O? obj, Type? type}) => + _getDataRepositoryImpl(obj: obj, type: type) ?? + DataRepositoryProvider.globalProvider + .getDataRepository(obj: obj, type: type); + + DataRepository? _getDataRepositoryImpl({O? obj, Type? type}) { + var dataRepository = _dataRepositories[O]; + + if (dataRepository == null && obj != null) { + dataRepository = _dataRepositories[obj.runtimeType]; + } + + if (dataRepository == null && type != null) { + dataRepository = _dataRepositories[type]; + } + + if (dataRepository != null) { + return dataRepository as DataRepository; + } + + dataRepository = + parentRepositoryProvider?.getDataRepository(obj: obj, type: type); + if (dataRepository != null) { + return dataRepository as DataRepository; + } + + for (var p in _knownDataRepositoryProviders) { + dataRepository = p.getDataRepository(obj: obj, type: type); + if (dataRepository != null) { + return dataRepository as DataRepository; + } + } + + return null; + } + + final Set _knownDataRepositoryProviders = + {}; + + @override + void notifyKnownDataRepositoryProvider(DataRepositoryProvider provider) { + _knownDataRepositoryProviders.add(provider); + } +} + +class SQLRepositoryAdapter with Initializable { + final SQLDatabaseAdapter databaseAdapter; + + final String name; + + final String tableName; + + final Type type; + + SQLRepositoryAdapter(this.databaseAdapter, this.name, + {String? tableName, Type? type}) + : tableName = tableName ?? name, + type = type ?? O; + + @override + void initialize() { + databaseAdapter.ensureInitialized(); + } + + String get dialect => databaseAdapter.dialect; + + FutureOr lengthSQL() { + var sql = databaseAdapter.generateLengthSQL(tableName); + return databaseAdapter.executeSQL(sql)!; + } + + SQL generateSelectSQL(EntityMatcher matcher, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) => + databaseAdapter.generateSelectSQL(tableName, matcher, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); + + FutureOr>> selectSQL(SQL sql, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) { + return databaseAdapter.selectSQL(tableName, sql, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); + } + + String generateInsertSQL(O o, Map fields) { + return databaseAdapter.generateInsertSQL(tableName, fields); + } + + Iterable insertSQL(String sql, Map fields) { + return databaseAdapter.executeSQL(sql)!; + } +} diff --git a/lib/src/bones_api_data_adapter_postgre.dart b/lib/src/bones_api_data_adapter_postgre.dart new file mode 100644 index 0000000..92e3b4a --- /dev/null +++ b/lib/src/bones_api_data_adapter_postgre.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:async_extension/async_extension.dart'; +import 'package:logging/logging.dart' as logging; +import 'package:postgres/postgres.dart'; + +import 'bones_api_data.dart'; +import 'bones_api_data_adapter.dart'; + +final _log = logging.Logger('PostgreAdapter'); + +class PostgreAdapter extends SQLDatabaseAdapter { + final String host; + final int port; + final String databaseName; + + final String username; + + final String? _password; + final PasswordProvider? _passwordProvider; + + PostgreAdapter(this.host, this.databaseName, this.username, + {String? password, + PasswordProvider? passwordProvider, + this.port = 5432, + int minConnections = 1, + int maxConnections = 3, + DataRepositoryProvider? parentRepositoryProvider}) + : _password = password, + _passwordProvider = passwordProvider, + super(minConnections, maxConnections, 'postgre', + parentRepositoryProvider: parentRepositoryProvider) { + if (_password == null && passwordProvider == null) { + throw ArgumentError("No `password` or `passwordProvider` "); + } + + parentRepositoryProvider?.notifyKnownDataRepositoryProvider(this); + } + + FutureOr _getPassword() { + if (_password != null) { + return _password!; + } else { + return _passwordProvider!(username); + } + } + + @override + FutureOr createConnection() async { + var password = await _getPassword(); + + var connection = PostgreSQLConnection(host, port, databaseName, + username: username, password: password); + await connection.open(); + + _log.log(logging.Level.INFO, 'createConnection> $connection'); + + return connection; + } + + @override + FutureOr isConnectionValid(PostgreSQLConnection connection) { + if (connection.isClosed) { + return false; + } + return true; + } + + @override + FutureOr>> doSelectSQL( + String table, SQL sql, PostgreSQLConnection connection, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) { + return connection + .mappedResultsQuery(sql.sql, substitutionValues: sql.parameters) + .resolveMapped((results) { + var entries = + results.map((e) => e[table]).whereType>(); + + return entries; + }); + } +} diff --git a/lib/src/bones_api_data_sql.dart b/lib/src/bones_api_data_sql.dart new file mode 100644 index 0000000..cc7dfc6 --- /dev/null +++ b/lib/src/bones_api_data_sql.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:bones_api/bones_api.dart'; + +import 'package:async_extension/async_extension.dart'; + +import 'bones_api_data_adapter.dart'; + +class DataRepositorySQL extends DataRepository with DataFieldAccessor { + final SQLRepositoryAdapter sqlRepositoryAdapter; + + DataRepositorySQL(this.sqlRepositoryAdapter, DataRepositoryProvider? provider, + String name, DataHandler dataHandler, + {Type? type}) + : super(provider, name, dataHandler, type: type); + + @override + void initialize() { + sqlRepositoryAdapter.ensureInitialized(); + } + + @override + Map information() => + {'queryType': 'SQL', 'dialect': sqlRepositoryAdapter.dialect}; + + @override + dynamic ensureStored(o) { + var id = getID(o, dataHandler: dataHandler); + + if (id == null) { + return store(o); + } else { + ensureReferencesStored(o); + } + + return id; + } + + @override + void ensureReferencesStored(o) { + for (var fieldName in dataHandler.fieldsNames(o)) { + var value = dataHandler.getField(o, fieldName); + if (value == null) { + continue; + } + + var repository = provider.getDataRepository(obj: value); + if (repository == null) { + continue; + } + + repository.ensureStored(value); + } + } + + @override + FutureOr length() => sqlRepositoryAdapter.lengthSQL(); + + @override + FutureOr> select(EntityMatcher matcher, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) { + var sql = sqlRepositoryAdapter.generateSelectSQL(matcher, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); + + var selRet = sqlRepositoryAdapter.selectSQL(sql, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); + + return selRet.resolveMapped((sel) { + var entities = sel.map((e) => dataHandler.createFromMap(e)).toList(); + return entities.resolveAll(); + }); + } + + @override + dynamic store(O o) { + var fields = dataHandler.getFields(o); + var sql = sqlRepositoryAdapter.generateInsertSQL(o, fields); + return sqlRepositoryAdapter.insertSQL(sql, fields); + } + + @override + Iterable storeAll(Iterable os) { + return os.map((o) => store(o)).toList(); + } +} diff --git a/lib/src/bones_api_extension.dart b/lib/src/bones_api_extension.dart index 1a063ea..277024b 100644 --- a/lib/src/bones_api_extension.dart +++ b/lib/src/bones_api_extension.dart @@ -1,4 +1,6 @@ -import 'package:reflection_factory/builder.dart'; +import 'dart:async'; + +import 'package:reflection_factory/reflection_factory.dart'; import 'bones_api_base.dart'; import 'bones_api_data.dart'; @@ -11,12 +13,12 @@ extension ClassReflectionExtension on ClassReflection { /// Lists the API methods of this reflected class. /// See [MethodReflectionExtension.isAPIMethod]. - List> apiMethods() => + List> apiMethods() => allMethods().where((m) => m.isAPIMethod).toList(); } /// [MethodReflection] extension. -extension MethodReflectionExtension on MethodReflection { +extension MethodReflectionExtension on MethodReflection { /// Returns `true` if this reflected method is an API method ([returnsAPIResponse] OR [receivesAPIRequest]). bool get isAPIMethod => returnsAPIResponse || receivesAPIRequest; @@ -27,5 +29,14 @@ extension MethodReflectionExtension on MethodReflection { bool get receivesAPIRequest => equalsNormalParametersTypes([APIRequest]); /// Returns `true` if this reflected method returns an [APIResponse]. - bool get returnsAPIResponse => returnType == APIResponse; + bool get returnsAPIResponse { + var returnType = this.returnType; + if (returnType == null) return false; + + var type = returnType.type; + + return type == APIResponse || + ((type == Future || type == FutureOr) && + (returnType.equalsArgumentsTypes([APIResponse]))); + } } diff --git a/lib/src/bones_api_hotreload_vm.dart b/lib/src/bones_api_hotreload_vm.dart index cb965d6..f7f05d6 100644 --- a/lib/src/bones_api_hotreload_vm.dart +++ b/lib/src/bones_api_hotreload_vm.dart @@ -31,11 +31,13 @@ class APIHotReloadVM extends APIHotReload { HotReloader.logLevel = logging.Level.CONFIG; _hotReloader = await HotReloader.create(onBeforeReload: (ctx) { var isolateId = ctx.isolate.id; + var isolateName = ctx.isolate.name ?? '?'; var ignore = isIgnoredIsolate(isolateId); if (ignore) { - _log.info('Hot-reload ignored for Isolate: `$isolateId`'); + _log.info( + 'Hot-reload ignored for Isolate[$isolateName]: `$isolateId`'); } else { - _log.info('Hot-reloading Isolate: `$isolateId`'); + _log.info('Hot-reloading Isolate[$isolateName]: `$isolateId`'); } return !ignore; }); diff --git a/lib/src/bones_api_mixin.dart b/lib/src/bones_api_mixin.dart new file mode 100644 index 0000000..3ca7ba1 --- /dev/null +++ b/lib/src/bones_api_mixin.dart @@ -0,0 +1,206 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:async_extension/async_extension.dart'; + +mixin Initializable { + bool _initialized = false; + + void ensureInitialized() { + if (_initialized) { + return; + } + + _initialized = true; + + initialize(); + } + + void initialize() {} +} + +class PoolTimeoutError extends Error { + final String message; + + PoolTimeoutError(this.message); + + @override + String toString() { + return 'PoolTimeoutError: $message'; + } +} + +mixin Pool { + final ListQueue _pool = ListQueue(8); + + Iterable get poolElements => List.unmodifiable(_pool); + + bool removeFromPool(O o) => _pool.remove(o); + + int removeElementsFromPool(int amount) { + var rm = 0; + + while (amount > 0 && _pool.isNotEmpty) { + _pool.removeFirst(); + --amount; + rm++; + } + + return rm; + } + + FutureOr> validPoolElements() => + filterPoolElements(isPoolElementValid); + + FutureOr> invalidPoolElements() => filterPoolElements( + (o) => isPoolElementValid(o).resolveMapped((valid) => !valid)); + + FutureOr> filterPoolElements( + FutureOr Function(O o) filter) async { + var elements = []; + + for (var o in _pool) { + await filter(o).resolveMapped((valid) { + if (valid) { + elements.add(o); + } + }); + } + + return elements; + } + + FutureOr removeInvalidElementsFromPool() { + FutureOr> ret = invalidPoolElements(); + + return ret.resolveMapped((l) { + for (var o in l) { + removeFromPool(o); + } + return true; + }); + } + + FutureOr isPoolElementValid(O o); + + FutureOr clearPool() { + _pool.clear(); + return true; + } + + int get poolSize => _pool.length; + + FutureOr createPoolElement(); + + Completer? _waitingPoolElement; + + FutureOr catchFromPool({Duration? timeout}) { + if (_pool.isEmpty) { + return _catchFromEmptyPool(timeout); + } else { + return _catchFromPopulatedPool(); + } + } + + FutureOr _catchFromEmptyPool(Duration? timeout) { + return createPoolElement().resolveMapped((o) { + if (o != null) return o; + + var waitingPoolElement = _waitingPoolElement ??= Completer(); + + var ret = waitingPoolElement.future.then((_) { + if (_pool.isNotEmpty) { + return _catchFromPopulatedPool(); + } else { + return _waitElementInPool(); + } + }); + + if (timeout != null) { + return ret.timeout(timeout, onTimeout: () { + throw PoolTimeoutError("Catch from Pool timeout[$timeout]: $this"); + }); + } else { + return ret; + } + }); + } + + FutureOr _catchFromPopulatedPool() { + var o = _pool.removeLast(); + _waitingPoolElement = null; + return preparePoolElement(o); + } + + FutureOr _waitElementInPool() async { + while (true) { + var waitingPoolElement = _waitingPoolElement ??= Completer(); + + await waitingPoolElement.future; + + if (_pool.isNotEmpty) { + return _catchFromPopulatedPool(); + } + } + } + + FutureOr preparePoolElement(O o) => o; + + DateTime _lastCheckPoolTime = DateTime.now(); + + int get lastCheckPoolElapsedTimeMs => + DateTime.now().millisecondsSinceEpoch - + _lastCheckPoolTime.millisecondsSinceEpoch; + + FutureOr callCheckPool() { + return checkPool().resolveMapped((ok) { + _lastCheckPoolTime = DateTime.now(); + return ok; + }); + } + + FutureOr checkPool() => true; + + FutureOr checkPoolSize( + int minSize, int maxSize, int checkInvalidsIntervalMs) { + var poolSize = this.poolSize; + + if (poolSize <= minSize) return true; + + if (poolSize > maxSize) { + return removeInvalidElementsFromPool().resolveMapped((_) { + var excess = this.poolSize - maxSize; + removeElementsFromPool(excess); + return true; + }); + } + + if (lastCheckPoolElapsedTimeMs > checkInvalidsIntervalMs) { + return removeInvalidElementsFromPool(); + } else { + return true; + } + } + + FutureOr recyclePoolElement(O o) => o; + + FutureOr releaseIntoPool(O o) { + var ret = recyclePoolElement(o); + + return ret.resolveMapped((recycled) { + if (recycled != null) { + checkPool(); + _pool.addLast(recycled); + + var waitingPoolElement = _waitingPoolElement; + if (waitingPoolElement != null && !waitingPoolElement.isCompleted) { + waitingPoolElement.complete(true); + } + + return true; + } else { + return false; + } + }); + } +} diff --git a/lib/src/bones_api_repository.dart b/lib/src/bones_api_repository.dart index a88be08..00538b2 100644 --- a/lib/src/bones_api_repository.dart +++ b/lib/src/bones_api_repository.dart @@ -1,11 +1,28 @@ +import 'dart:async'; + import 'bones_api_data.dart'; /// A data repository API. abstract class APIRepository { + /// Resolves a [DataRepository]. + static DataRepository? resolveDataRepository( + {DataRepository? dataRepository, + DataRepositoryProvider? provider, + Type? type}) { + return dataRepository ?? + provider?.getDataRepository(type: type) ?? + DataRepositoryProvider.globalProvider.getDataRepository(type: type); + } + final DataRepository dataRepository; - APIRepository(this.dataRepository) { - dataRepository.ensureInitialized(); + APIRepository( + {DataRepository? dataRepository, + DataRepositoryProvider? provider, + Type? type}) + : dataRepository = resolveDataRepository( + dataRepository: dataRepository, provider: provider, type: type)! { + this.dataRepository.ensureInitialized(); } void configure(); @@ -17,4 +34,17 @@ abstract class APIRepository { _configured = true; configure(); } + + FutureOr selectByID(dynamic id) => dataRepository.selectByID(id); + + FutureOr length() => dataRepository.length(); + + FutureOr> selectByQuery(String query, + {Object? parameters, + List? positionalParameters, + Map? namedParameters}) => + dataRepository.selectByQuery(query, + parameters: parameters, + positionalParameters: positionalParameters, + namedParameters: namedParameters); } diff --git a/lib/src/bones_api_server.dart b/lib/src/bones_api_server.dart index 33271ca..42fb13f 100644 --- a/lib/src/bones_api_server.dart +++ b/lib/src/bones_api_server.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:async_extension/async_extension.dart'; import 'package:bones_api/bones_api.dart'; import 'package:logging/logging.dart' as logging; +import 'package:mime/mime.dart'; import 'package:reflection_factory/reflection_factory.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; @@ -221,21 +222,31 @@ class APIServer { if (payload == null) return null; if (payload is String) { - apiResponse.payloadMimeType ??= resolveBestTextMimeType(payload); + apiResponse.payloadMimeType ??= + resolveBestTextMimeType(payload, apiResponse.payloadFileExtension); return payload; } if (payload is List) { - apiResponse.payloadMimeType ??= 'application/octet-stream'; + apiResponse.payloadMimeType ??= lookupMimeType( + apiResponse.payloadFileExtension ?? 'bytes', + headerBytes: payload) ?? + 'application/octet-stream'; return payload; } if (payload is Stream>) { - apiResponse.payloadMimeType ??= 'application/octet-stream'; + apiResponse.payloadMimeType ??= + lookupMimeType(apiResponse.payloadFileExtension ?? 'bytes') ?? + 'application/octet-stream'; + return payload; } if (payload is DateTime) { + apiResponse.payloadMimeType ??= + lookupMimeType(apiResponse.payloadFileExtension ?? 'text') ?? + 'text/plain'; return payload.toString(); } @@ -246,14 +257,22 @@ class APIServer { return s; } catch (e) { var s = payload.toString(); - apiResponse.payloadMimeType ??= resolveBestTextMimeType(s); + apiResponse.payloadMimeType ??= + resolveBestTextMimeType(s, apiResponse.payloadFileExtension); return s; } } static final RegExp _htmlTag = RegExp(r'<\w+.*?>'); - static String resolveBestTextMimeType(String text) { + static String resolveBestTextMimeType(String text, [String? fileExtension]) { + if (fileExtension != null && fileExtension.isNotEmpty) { + var mimeType = lookupMimeType(fileExtension); + if (mimeType != null) { + return mimeType; + } + } + if (text.contains('<')) { if (_htmlTag.hasMatch(text)) { return 'text/html'; diff --git a/pubspec.yaml b/pubspec.yaml index 0313063..46e65df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - Simple and easy API framework, with routes and HTTP Server. -version: 1.0.6 +version: 1.0.7 homepage: https://github.com/Colossus-Services/bones_api environment: @@ -14,11 +14,14 @@ dependencies: async_extension: ^1.0.4 dart_spawner: ^1.0.5 args: ^2.2.0 - reflection_factory: ^1.0.4 + reflection_factory: ^1.0.5 petitparser: ^4.2.0 hotreloader: ^3.0.1 logging: ^1.0.1 collection: ^1.15.0 + mime: ^1.0.0 + postgres: ^2.4.1+2 + dev_dependencies: lints: ^1.0.1 diff --git a/test/bones_api_data_test.dart b/test/bones_api_data_test.dart index a950371..3377324 100644 --- a/test/bones_api_data_test.dart +++ b/test/bones_api_data_test.dart @@ -6,8 +6,8 @@ void main() { setUp(() {}); test('basic', () async { - var repository = - SetDataRepository('user', EntityDataHandler()); + var repository = SetDataRepository('user', + EntityDataHandler(instantiatorFromMap: (m) => User.fromMap(m))); expect(repository.nextID(), equals(1)); expect(repository.selectByID(1), isNull); @@ -81,8 +81,9 @@ void main() { equals([user2])); expect( - repository.selectByQuery('address.state == ?', - parameters: {'state': 'FL'}).map((e) => e.toJsonEncoded()), + (await repository.selectByQuery('address.state == ?', + parameters: {'state': 'FL'})) + .map((e) => e.toJsonEncoded()), isEmpty); }); }); @@ -99,6 +100,12 @@ class User extends DataEntity { User(this.email, this.password, this.address, {this.id}); + User.fromMap(Map map) + : email = map['email'], + password = map['email'], + address = map['address'], + id = map['id']; + @override bool operator ==(Object other) => identical(this, other) || @@ -127,6 +134,22 @@ class User extends DataEntity { } } + @override + Type? getFieldType(String key) { + switch (key) { + case 'id': + return int; + case 'email': + return String; + case 'password': + return String; + case 'address': + return Address; + default: + return null; + } + } + @override void setField(String key, V? value) { switch (key) { @@ -224,6 +247,24 @@ class Address extends DataEntity { } } + @override + Type? getFieldType(String key) { + switch (key) { + case 'id': + return int; + case 'state': + return String; + case 'city': + return String; + case 'street': + return String; + case 'number': + return int; + default: + return null; + } + } + @override void setField(String key, V? value) { switch (key) {