diff --git a/README.md b/README.md index ba42e1cd..9d8e5d0d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # relations -[![version 1.0.0-alpha](https://img.shields.io/badge/pub-v1.0.0--alpha-red.svg)](https://pub.dartlang.org/packages/angel_relations) +[![version 1.0.0](https://img.shields.io/badge/pub-v1.0.0-brightgreen.svg)](https://pub.dartlang.org/packages/angel_relations) [![build status](https://travis-ci.org/angel-dart/relations.svg)](https://travis-ci.org/angel-dart/relations) Database-agnostic relations between Angel services. diff --git a/lib/src/belongs_to.dart b/lib/src/belongs_to.dart index acf7d110..1f764343 100644 --- a/lib/src/belongs_to.dart +++ b/lib/src/belongs_to.dart @@ -17,20 +17,19 @@ HookedServiceEventListener belongsTo(Pattern servicePath, String localKey, getForeignKey(obj), assignForeignObject(foreign, obj)}) { + String localId = localKey; + var foreignName = + as?.isNotEmpty == true ? as : pluralize.singular(servicePath.toString()); + + if (localId == null) { + localId = foreignName + 'Id'; + // print('No local key provided for belongsTo, defaulting to \'$localId\'.'); + } + return (HookedServiceEvent e) async { var ref = e.service.app.service(servicePath); - var foreignName = as?.isNotEmpty == true - ? as - : pluralize.singular(servicePath.toString()); if (ref == null) throw noService(servicePath); - String localId = localKey; - - if (localId == null) { - localId = foreignName + 'Id'; - print('No local key provided for belongsTo, defaulting to \'$localId\'.'); - } - _getForeignKey(obj) { if (getForeignKey != null) return getForeignKey(obj); @@ -62,7 +61,7 @@ HookedServiceEventListener belongsTo(Pattern servicePath, 'query': {foreignKey ?? 'id': id} }); - if (indexed?.isNotEmpty != true) { + if (indexed == null || indexed is! List || indexed.isNotEmpty != true) { await _assignForeignObject(null, obj); } else { var child = indexed.first; diff --git a/lib/src/belongs_to_many.dart b/lib/src/belongs_to_many.dart new file mode 100644 index 00000000..2b3a508a --- /dev/null +++ b/lib/src/belongs_to_many.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:mirrors'; +import 'package:angel_framework/angel_framework.dart'; +import 'plural.dart' as pluralize; +import 'no_service.dart'; + +/// Represents a relationship in which the current [service] "belongs to" +/// multiple members of the service at [servicePath]. Use [as] to set the name +/// on the target object. +/// +/// Defaults: +/// * [foreignKey]: `userId` +/// * [localKey]: `id` +HookedServiceEventListener belongsToMany(Pattern servicePath, + {String as, + String foreignKey, + String localKey, + getForeignKey(obj), + assignForeignObject(List foreign, obj)}) { + String localId = localKey; + var foreignName = + as?.isNotEmpty == true ? as : pluralize.plural(servicePath.toString()); + + if (localId == null) { + localId = foreignName + 'Id'; + // print('No local key provided for belongsToMany, defaulting to \'$localId\'.'); + } + + return (HookedServiceEvent e) async { + var ref = e.service.app.service(servicePath); + if (ref == null) throw noService(servicePath); + + _getForeignKey(obj) { + if (getForeignKey != null) + return getForeignKey(obj); + else if (obj is Map) + return obj[localId]; + else if (obj is Extensible) + return obj.properties[localId]; + else if (localId == null || localId == 'userId') + return obj.userId; + else + return reflect(obj).getField(new Symbol(localId)).reflectee; + } + + _assignForeignObject(foreign, obj) { + if (assignForeignObject != null) + return assignForeignObject(foreign, obj); + else if (obj is Map) + obj[foreignName] = foreign; + else if (obj is Extensible) + obj.properties[foreignName] = foreign; + else + reflect(obj).setField(new Symbol(foreignName), foreign); + } + + _normalize(obj) async { + if (obj != null) { + var id = await _getForeignKey(obj); + var indexed = await ref.index({ + 'query': {foreignKey ?? 'id': id} + }); + + if (indexed == null || indexed is! List || indexed.isNotEmpty != true) { + await _assignForeignObject(null, obj); + } else { + var child = indexed is Iterable ? indexed.toList() : [indexed]; + await _assignForeignObject(child, obj); + } + } + } + + if (e.result is Iterable) { + await Future.wait(e.result.map(_normalize)); + } else + await _normalize(e.result); + }; +} diff --git a/lib/src/has_many.dart b/lib/src/has_many.dart index 1ca1f58c..34cffcff 100644 --- a/lib/src/has_many.dart +++ b/lib/src/has_many.dart @@ -17,16 +17,13 @@ HookedServiceEventListener hasMany(Pattern servicePath, String localKey, getLocalKey(obj), assignForeignObjects(foreign, obj)}) { + return (HookedServiceEvent e) async { var ref = e.service.app.service(servicePath); var foreignName = as?.isNotEmpty == true ? as : pluralize.plural(servicePath.toString()); if (ref == null) throw noService(servicePath); - if (foreignKey == null) - print( - 'WARNING: No foreign key provided for hasMany, defaulting to \'userId\'.'); - _getLocalKey(obj) { if (getLocalKey != null) return getLocalKey(obj); @@ -58,7 +55,7 @@ HookedServiceEventListener hasMany(Pattern servicePath, 'query': {foreignKey ?? 'userId': id} }); - if (indexed?.isNotEmpty != true) { + if (indexed == null || indexed is! List || indexed.isNotEmpty != true) { await _assignForeignObjects([], obj); } else { await _assignForeignObjects(indexed, obj); diff --git a/lib/src/has_many_through.dart b/lib/src/has_many_through.dart new file mode 100644 index 00000000..9539519e --- /dev/null +++ b/lib/src/has_many_through.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:mirrors'; +import 'package:angel_framework/angel_framework.dart'; +import 'plural.dart' as pluralize; +import 'no_service.dart'; + +HookedServiceEventListener hasManyThrough(String servicePath, String pivotPath, + {String as, + String localKey, + String pivotKey, + String foreignKey, + getLocalKey(obj), + getPivotKey(obj), + getForeignKey(obj), + assignForeignObjects(foreign, obj)}) { + var foreignName = + as?.isNotEmpty == true ? as : pluralize.plural(servicePath.toString()); + + return (HookedServiceEvent e) async { + var pivotService = e.getService(pivotPath); + var foreignService = e.getService(servicePath); + + if (pivotService == null) + throw noService(pivotPath); + else if (foreignService == null) throw noService(servicePath); + + _assignForeignObjects(foreign, obj) { + if (assignForeignObjects != null) + return assignForeignObjects(foreign, obj); + else if (obj is Map) + obj[foreignName] = foreign; + else if (obj is Extensible) + obj.properties[foreignName] = foreign; + else + reflect(obj).setField(new Symbol(foreignName), foreign); + } + + _getLocalKey(obj) { + if (getLocalKey != null) + return getLocalKey(obj); + else if (obj is Map) + return obj[localKey ?? 'id']; + else if (obj is Extensible) + return obj.properties[localKey ?? 'id']; + else if (localKey == null || localKey == 'id') + return obj.id; + else + return reflect(obj).getField(new Symbol(localKey ?? 'id')).reflectee; + } + + _getPivotKey(obj) { + if (getPivotKey != null) + return getPivotKey(obj); + else if (obj is Map) + return obj[pivotKey ?? 'id']; + else if (obj is Extensible) + return obj.properties[pivotKey ?? 'id']; + else if (pivotKey == null || pivotKey == 'id') + return obj.id; + else + return reflect(obj).getField(new Symbol(pivotKey ?? 'id')).reflectee; + } + + _normalize(obj) async { + // First, resolve pivot + var id = await _getLocalKey(obj); + var indexed = await pivotService.index({ + 'query': {pivotKey ?? 'userId': id} + }); + + if (indexed == null || indexed is! List || indexed.isNotEmpty != true) { + await _assignForeignObjects([], obj); + } else { + // Now, resolve from foreign service + var mapped = await Future.wait(indexed.map((pivot) async { + var id = await _getPivotKey(obj); + var indexed = await foreignService.index({ + 'query': {foreignKey ?? 'postId': id} + }); + + if (indexed == null || + indexed is! List || + indexed.isNotEmpty != true) { + await _assignForeignObjects([], pivot); + } else { + await _assignForeignObjects(indexed, pivot); + } + + return pivot; + })); + await _assignForeignObjects(mapped, obj); + } + } + + if (e.result is Iterable) { + await Future.wait(e.result.map(_normalize)); + } else + await _normalize(e.result); + }; +} diff --git a/lib/src/has_one.dart b/lib/src/has_one.dart index d8e3112b..d1debf11 100644 --- a/lib/src/has_one.dart +++ b/lib/src/has_one.dart @@ -17,6 +17,7 @@ HookedServiceEventListener hasOne(Pattern servicePath, String localKey, getLocalKey(obj), assignForeignObject(foreign, obj)}) { + return (HookedServiceEvent e) async { var ref = e.service.app.service(servicePath); var foreignName = as?.isNotEmpty == true @@ -24,10 +25,6 @@ HookedServiceEventListener hasOne(Pattern servicePath, : pluralize.singular(servicePath.toString()); if (ref == null) throw noService(servicePath); - if (foreignKey == null) - print( - 'WARNING: No foreign key provided for hasOne, defaulting to \'userId\'.'); - _getLocalKey(obj) { if (getLocalKey != null) return getLocalKey(obj); @@ -59,7 +56,7 @@ HookedServiceEventListener hasOne(Pattern servicePath, 'query': {foreignKey ?? 'userId': id} }); - if (indexed?.isNotEmpty != true) { + if (indexed == null || indexed is! List || indexed.isNotEmpty != true) { await _assignForeignObject(null, obj); } else { var child = indexed.first; diff --git a/pubspec.yaml b/pubspec.yaml index 4163d79d..171ba028 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ author: Tobe O description: Database-agnostic relations between Angel services. homepage: "https://github.com/angel-dart/relations.git" name: angel_relations -version: 1.0.0-alpha +version: 1.0.0 environment: sdk: ">=1.19.0" dependencies: diff --git a/test/belongs_to_test.dart b/test/belongs_to_test.dart index d398e923..6bdb7aca 100644 --- a/test/belongs_to_test.dart +++ b/test/belongs_to_test.dart @@ -9,8 +9,8 @@ main() { setUp(() async { app = new Angel() - ..use('/authors', new MapService()) - ..use('/books', new MapService()); + ..use('/authors', new CustomMapService()) + ..use('/books', new CustomMapService()); await app.configure(seed( 'authors', diff --git a/test/common.dart b/test/common.dart index 65b48d28..4c319d51 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,7 +1,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; -class MapService extends Service { +class CustomMapService extends Service { final List _items = []; Iterable tailor(Iterable items, Map params) { diff --git a/test/has_many_test.dart b/test/has_many_test.dart index e27b19e4..efd13cd3 100644 --- a/test/has_many_test.dart +++ b/test/has_many_test.dart @@ -9,8 +9,8 @@ main() { setUp(() async { app = new Angel() - ..use('/authors', new MapService()) - ..use('/books', new MapService()); + ..use('/authors', new CustomMapService()) + ..use('/books', new CustomMapService()); await app.configure(seed( 'authors', diff --git a/test/has_one_test.dart b/test/has_one_test.dart index a5aed15c..be3f2dc0 100644 --- a/test/has_one_test.dart +++ b/test/has_one_test.dart @@ -9,8 +9,8 @@ main() { setUp(() async { app = new Angel() - ..use('/authors', new MapService()) - ..use('/books', new MapService()); + ..use('/authors', new CustomMapService()) + ..use('/books', new CustomMapService()); await app.configure(seed( 'authors',