From d8304b91043f1c8a2363b398b251393588ce8dfc Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sun, 29 Jan 2017 15:23:53 -0500 Subject: [PATCH 1/6] Initial commit --- .gitignore | 27 +++++++++++++++++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 50 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7c280441 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..89074fd3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 The Angel Framework + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..e754f7b1 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# relations +Database-agnostic relations between Angel services. From 160c620f57a62cbf59328d657de9cae221c53c07 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 29 Jan 2017 21:39:11 -0500 Subject: [PATCH 2/6] alpha --- .gitignore | 1 + .travis.yml | 1 + README.md | 21 +++++++++++ lib/angel_relations.dart | 7 ++++ lib/src/belongs_to.dart | 79 +++++++++++++++++++++++++++++++++++++++ lib/src/has_many.dart | 74 ++++++++++++++++++++++++++++++++++++ lib/src/has_one.dart | 76 +++++++++++++++++++++++++++++++++++++ lib/src/no_service.dart | 2 + lib/src/plural.dart | 21 +++++++++++ pubspec.yaml | 12 ++++++ test/belongs_to_test.dart | 56 +++++++++++++++++++++++++++ test/common.dart | 67 +++++++++++++++++++++++++++++++++ test/has_many_test.dart | 61 ++++++++++++++++++++++++++++++ test/has_one_test.dart | 57 ++++++++++++++++++++++++++++ 14 files changed, 535 insertions(+) create mode 100644 .travis.yml create mode 100644 lib/angel_relations.dart create mode 100644 lib/src/belongs_to.dart create mode 100644 lib/src/has_many.dart create mode 100644 lib/src/has_one.dart create mode 100644 lib/src/no_service.dart create mode 100644 lib/src/plural.dart create mode 100644 pubspec.yaml create mode 100644 test/belongs_to_test.dart create mode 100644 test/common.dart create mode 100644 test/has_many_test.dart create mode 100644 test/has_one_test.dart diff --git a/.gitignore b/.gitignore index 7c280441..427e911b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .packages .project .pub/ +.scripts-bin/ build/ **/packages/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/README.md b/README.md index e754f7b1..ba42e1cd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # 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) +[![build status](https://travis-ci.org/angel-dart/relations.svg)](https://travis-ci.org/angel-dart/relations) + Database-agnostic relations between Angel services. + +```dart +// Authors owning one book +app.service('authors').afterAll( + relations.hasOne('books', as: 'book', foreignKey: 'authorId')); + +// Or multiple +app.service('authors').afterAll( + relations.hasMany('books', foreignKey: 'authorId')); + +// Or, books belonging to authors +app.service('books').afterAll(relations.belongsTo('authors')); +``` + +Currently supports: +* `hasOne` +* `hasMany` +* `belongsTo` \ No newline at end of file diff --git a/lib/angel_relations.dart b/lib/angel_relations.dart new file mode 100644 index 00000000..f359f819 --- /dev/null +++ b/lib/angel_relations.dart @@ -0,0 +1,7 @@ +/// Hooks to populate data returned from services, in a fashion +/// reminiscent of a relational database. +library angel_relations; + +export 'src/belongs_to.dart'; +export 'src/has_many.dart'; +export 'src/has_one.dart'; \ No newline at end of file diff --git a/lib/src/belongs_to.dart b/lib/src/belongs_to.dart new file mode 100644 index 00000000..acf7d110 --- /dev/null +++ b/lib/src/belongs_to.dart @@ -0,0 +1,79 @@ +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" +/// a single member of the service at [servicePath]. Use [as] to set the name +/// on the target object. +/// +/// Defaults: +/// * [foreignKey]: `userId` +/// * [localKey]: `id` +HookedServiceEventListener belongsTo(Pattern servicePath, + {String as, + String foreignKey, + String localKey, + getForeignKey(obj), + assignForeignObject(foreign, obj)}) { + 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); + 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?.isNotEmpty != true) { + await _assignForeignObject(null, obj); + } else { + var child = indexed.first; + 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 new file mode 100644 index 00000000..1ca1f58c --- /dev/null +++ b/lib/src/has_many.dart @@ -0,0 +1,74 @@ +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] "owns" +/// members of the service at [servicePath]. Use [as] to set the name +/// on the target object. +/// +/// Defaults: +/// * [foreignKey]: `userId` +/// * [localKey]: `id` +HookedServiceEventListener hasMany(Pattern servicePath, + {String as, + String foreignKey, + 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); + 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; + } + + _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); + } + + _normalize(obj) async { + if (obj != null) { + var id = await _getLocalKey(obj); + var indexed = await ref.index({ + 'query': {foreignKey ?? 'userId': id} + }); + + if (indexed?.isNotEmpty != true) { + await _assignForeignObjects([], obj); + } else { + await _assignForeignObjects(indexed, 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 new file mode 100644 index 00000000..d8e3112b --- /dev/null +++ b/lib/src/has_one.dart @@ -0,0 +1,76 @@ +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] "owns" +/// a single member of the service at [servicePath]. Use [as] to set the name +/// on the target object. +/// +/// Defaults: +/// * [foreignKey]: `userId` +/// * [localKey]: `id` +HookedServiceEventListener hasOne(Pattern servicePath, + {String as, + String foreignKey, + String localKey, + getLocalKey(obj), + assignForeignObject(foreign, obj)}) { + 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); + + if (foreignKey == null) + print( + 'WARNING: No foreign key provided for hasOne, defaulting to \'userId\'.'); + + _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; + } + + _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 _getLocalKey(obj); + var indexed = await ref.index({ + 'query': {foreignKey ?? 'userId': id} + }); + + if (indexed?.isNotEmpty != true) { + await _assignForeignObject(null, obj); + } else { + var child = indexed.first; + 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/no_service.dart b/lib/src/no_service.dart new file mode 100644 index 00000000..e9d78658 --- /dev/null +++ b/lib/src/no_service.dart @@ -0,0 +1,2 @@ +ArgumentError noService(Pattern path) => + new ArgumentError("No service exists at path '$path'."); \ No newline at end of file diff --git a/lib/src/plural.dart b/lib/src/plural.dart new file mode 100644 index 00000000..972d2fdd --- /dev/null +++ b/lib/src/plural.dart @@ -0,0 +1,21 @@ +String singular(String path) { + var str = path.trim().split('/').where((str) => str.isNotEmpty).last; + + if (str.endsWith('ies')) + return str.substring(0, str.length - 3) + 'y'; + else if (str.endsWith('s')) + return str.substring(0, str.length - 1); + else + return str; +} + +String plural(String path) { + var str = path.trim().split('/').where((str) => str.isNotEmpty).last; + + if (str.endsWith('y')) + return str.substring(0, str.length - 1) + 'ies'; + else if (str.endsWith('s')) + return str; + else + return str + 's'; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..c1792cbf --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,12 @@ +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 +environment: + sdk: ">=1.19.0" +dependencies: + angel_framework: "^1.0.0-dev" +dev_dependencies: + angel_seeder: ^0.0.0 + test: ^0.12.0 diff --git a/test/belongs_to_test.dart b/test/belongs_to_test.dart new file mode 100644 index 00000000..d398e923 --- /dev/null +++ b/test/belongs_to_test.dart @@ -0,0 +1,56 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_relations/angel_relations.dart' as relations; +import 'package:angel_seeder/angel_seeder.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + Angel app; + + setUp(() async { + app = new Angel() + ..use('/authors', new MapService()) + ..use('/books', new MapService()); + + await app.configure(seed( + 'authors', + new SeederConfiguration( + count: 10, + template: {'name': (Faker faker) => faker.person.name()}, + callback: (Map author, seed) { + return seed( + 'books', + new SeederConfiguration(delete: false, count: 10, template: { + 'authorId': author['id'], + 'title': (Faker faker) => + 'I love to eat ${faker.food.dish()}' + })); + }))); + + app.service('books').afterAll(relations.belongsTo('authors')); + }); + + test('index', () async { + var books = await app.service('books').index(); + print(books); + + expect(books, allOf(isList, isNotEmpty)); + + for (Map book in books) { + expect(book.keys, contains('author')); + + Map author = book['author']; + expect(author['id'], equals(book['authorId'])); + } + }); + + test('create', () async { + var warAndPeace = await app + .service('books') + .create(new Book(title: 'War and Peace').toJson()); + + print(warAndPeace); + expect(warAndPeace.keys, contains('author')); + expect(warAndPeace['author'], isNull); + }); +} diff --git a/test/common.dart b/test/common.dart new file mode 100644 index 00000000..65b48d28 --- /dev/null +++ b/test/common.dart @@ -0,0 +1,67 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:json_god/json_god.dart' as god; + +class MapService extends Service { + final List _items = []; + + Iterable tailor(Iterable items, Map params) { + if (params == null) return items; + + var r = items; + + if (params != null && params['query'] is Map) { + Map query = params['query']; + + for (var key in query.keys) { + r = r.where((m) => m[key] == query[key]); + } + } + + return r; + } + + @override + index([params]) async => tailor(_items, params).toList(); + + @override + read(id, [Map params]) async { + return tailor(_items, params).firstWhere((m) => m['id'] == id, + orElse: () => throw new AngelHttpException.notFound()); + } + + @override + create(data, [params]) async { + Map d = data is Map ? data : god.serializeObject(data); + d['id'] = _items.length.toString(); + _items.add(d); + return d; + } + + @override + remove(id, [params]) async { + if (id == null) _items.clear(); + } +} + +class Author { + String id, name; + + Author({this.id, this.name}); + + Map toJson() => {'id': id, 'name': name}; +} + +class Book { + String authorId, title; + + Book({this.authorId, this.title}); + + Map toJson() => {'authorId': authorId, 'title': title}; +} + +class Chapter { + String bookId, title; + int pageCount; + + Chapter({this.bookId, this.title, this.pageCount}); +} diff --git a/test/has_many_test.dart b/test/has_many_test.dart new file mode 100644 index 00000000..e27b19e4 --- /dev/null +++ b/test/has_many_test.dart @@ -0,0 +1,61 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_relations/angel_relations.dart' as relations; +import 'package:angel_seeder/angel_seeder.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + Angel app; + + setUp(() async { + app = new Angel() + ..use('/authors', new MapService()) + ..use('/books', new MapService()); + + await app.configure(seed( + 'authors', + new SeederConfiguration( + count: 10, + template: {'name': (Faker faker) => faker.person.name()}, + callback: (Map author, seed) { + return seed( + 'books', + new SeederConfiguration(delete: false, count: 10, template: { + 'authorId': author['id'], + 'title': (Faker faker) => + 'I love to eat ${faker.food.dish()}' + })); + }))); + + app + .service('authors') + .afterAll(relations.hasMany('books', foreignKey: 'authorId')); + }); + + test('index', () async { + var authors = await app.service('authors').index(); + print(authors); + + expect(authors, allOf(isList, isNotEmpty)); + + for (Map author in authors) { + expect(author.keys, contains('books')); + + List books = author['books']; + + for (var book in books) { + expect(book['authorId'], equals(author['id'])); + } + } + }); + + test('create', () async { + var tolstoy = await app + .service('authors') + .create(new Author(name: 'Leo Tolstoy').toJson()); + + print(tolstoy); + expect(tolstoy.keys, contains('books')); + expect(tolstoy['books'], allOf(isList, isEmpty)); + }); +} diff --git a/test/has_one_test.dart b/test/has_one_test.dart new file mode 100644 index 00000000..a5aed15c --- /dev/null +++ b/test/has_one_test.dart @@ -0,0 +1,57 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_relations/angel_relations.dart' as relations; +import 'package:angel_seeder/angel_seeder.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + Angel app; + + setUp(() async { + app = new Angel() + ..use('/authors', new MapService()) + ..use('/books', new MapService()); + + await app.configure(seed( + 'authors', + new SeederConfiguration( + count: 10, + template: {'name': (Faker faker) => faker.person.name()}, + callback: (Map author, seed) { + return seed( + 'books', + new SeederConfiguration(delete: false, count: 10, template: { + 'authorId': author['id'], + 'title': (Faker faker) => + 'I love to eat ${faker.food.dish()}' + })); + }))); + + app.service('authors').afterAll( + relations.hasOne('books', as: 'book', foreignKey: 'authorId')); + }); + + test('index', () async { + var authors = await app.service('authors').index(); + print(authors); + + expect(authors, allOf(isList, isNotEmpty)); + + for (Map author in authors) { + expect(author.keys, contains('book')); + + Map book = author['book']; + expect(book['authorId'], equals(author['id'])); + } + }); + + test('create', () async { + var tolstoy = await app + .service('authors') + .create(new Author(name: 'Leo Tolstoy').toJson()); + + print(tolstoy); + expect(tolstoy.keys, contains('book')); + expect(tolstoy['book'], isNull); + }); +} From 012c323e0c56d384c9e818a8173a3b1f1eaac0a5 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 29 Jan 2017 21:43:01 -0500 Subject: [PATCH 3/6] pubspec --- pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index c1792cbf..4163d79d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ -author: "Tobe O " -description: "Database-agnostic relations between Angel services." +author: Tobe O +description: Database-agnostic relations between Angel services. homepage: "https://github.com/angel-dart/relations.git" -name: "angel_relations" +name: angel_relations version: 1.0.0-alpha environment: sdk: ">=1.19.0" dependencies: - angel_framework: "^1.0.0-dev" + angel_framework: ^1.0.0-dev dev_dependencies: angel_seeder: ^0.0.0 test: ^0.12.0 From b67ddeb444eb94fcda72e9771b3fcaaba6e7a7d5 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 4 Mar 2017 16:00:17 -0500 Subject: [PATCH 4/6] 1.0.0 --- README.md | 2 +- lib/src/belongs_to.dart | 21 ++++--- lib/src/belongs_to_many.dart | 78 ++++++++++++++++++++++++++ lib/src/has_many.dart | 7 +-- lib/src/has_many_through.dart | 100 ++++++++++++++++++++++++++++++++++ lib/src/has_one.dart | 7 +-- pubspec.yaml | 2 +- test/belongs_to_test.dart | 4 +- test/common.dart | 2 +- test/has_many_test.dart | 4 +- test/has_one_test.dart | 4 +- 11 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 lib/src/belongs_to_many.dart create mode 100644 lib/src/has_many_through.dart 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', From 7f83004e07e1e4972ad6f3bec2102c721ff7e14e Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sat, 4 Mar 2017 16:05:56 -0500 Subject: [PATCH 5/6] 1.0.1 --- README.md | 8 +++++--- lib/angel_relations.dart | 2 ++ pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d8e5d0d..ea8a4e74 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # relations -[![version 1.0.0](https://img.shields.io/badge/pub-v1.0.0-brightgreen.svg)](https://pub.dartlang.org/packages/angel_relations) +[![version 1.0.1](https://img.shields.io/badge/pub-v1.0.1-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. @@ -17,7 +17,9 @@ app.service('authors').afterAll( app.service('books').afterAll(relations.belongsTo('authors')); ``` -Currently supports: +Supports: * `hasOne` * `hasMany` -* `belongsTo` \ No newline at end of file +* `hasManyThrough` +* `belongsTo` +* `belongsToMany` \ No newline at end of file diff --git a/lib/angel_relations.dart b/lib/angel_relations.dart index f359f819..5f2b6a7e 100644 --- a/lib/angel_relations.dart +++ b/lib/angel_relations.dart @@ -2,6 +2,8 @@ /// reminiscent of a relational database. library angel_relations; +export 'src/belongs_to_many.dart'; export 'src/belongs_to.dart'; export 'src/has_many.dart'; +export 'src/has_many_through.dart'; export 'src/has_one.dart'; \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 171ba028..f2ecb253 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 +version: 1.0.1 environment: sdk: ">=1.19.0" dependencies: From f1d7081f462d497a1d5810356132867158379e79 Mon Sep 17 00:00:00 2001 From: thosakwe Date: Sun, 9 Jul 2017 13:11:17 -0400 Subject: [PATCH 6/6] Needed no actual changes... --- .gitignore | 65 +++++++++++++++++++++++++++++++++++++++ lib/src/belongs_to.dart | 4 +-- lib/src/has_one.dart | 1 + pubspec.yaml | 2 +- test/belongs_to_test.dart | 4 +-- test/common.dart | 1 + test/has_many_test.dart | 4 +-- test/has_one_test.dart | 6 ++-- 8 files changed, 78 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 427e911b..e822a056 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,68 @@ doc/api/ # Don't commit pubspec lock file # (Library packages only! Remove pattern if developing an application package) pubspec.lock +### Dart template +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub + +# SDK 1.20 and later (no longer creates packages directories) + +# Older SDK versions +# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20) + + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) + +# Directory created by dartdoc + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/lib/src/belongs_to.dart b/lib/src/belongs_to.dart index 1f764343..68ae9c38 100644 --- a/lib/src/belongs_to.dart +++ b/lib/src/belongs_to.dart @@ -9,8 +9,8 @@ import 'no_service.dart'; /// on the target object. /// /// Defaults: -/// * [foreignKey]: `userId` -/// * [localKey]: `id` +/// * [localKey]: `userId` +/// * [foreignKey]: `id` HookedServiceEventListener belongsTo(Pattern servicePath, {String as, String foreignKey, diff --git a/lib/src/has_one.dart b/lib/src/has_one.dart index d1debf11..8578081b 100644 --- a/lib/src/has_one.dart +++ b/lib/src/has_one.dart @@ -52,6 +52,7 @@ HookedServiceEventListener hasOne(Pattern servicePath, _normalize(obj) async { if (obj != null) { var id = await _getLocalKey(obj); + var indexed = await ref.index({ 'query': {foreignKey ?? 'userId': id} }); diff --git a/pubspec.yaml b/pubspec.yaml index f2ecb253..d25318d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,5 +8,5 @@ environment: dependencies: angel_framework: ^1.0.0-dev dev_dependencies: - angel_seeder: ^0.0.0 + angel_seeder: ^1.0.0 test: ^0.12.0 diff --git a/test/belongs_to_test.dart b/test/belongs_to_test.dart index 6bdb7aca..d398e923 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 CustomMapService()) - ..use('/books', new CustomMapService()); + ..use('/authors', new MapService()) + ..use('/books', new MapService()); await app.configure(seed( 'authors', diff --git a/test/common.dart b/test/common.dart index 4c319d51..f8adf3dd 100644 --- a/test/common.dart +++ b/test/common.dart @@ -1,6 +1,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:json_god/json_god.dart' as god; +@deprecated class CustomMapService extends Service { final List _items = []; diff --git a/test/has_many_test.dart b/test/has_many_test.dart index efd13cd3..e27b19e4 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 CustomMapService()) - ..use('/books', new CustomMapService()); + ..use('/authors', new MapService()) + ..use('/books', new MapService()); await app.configure(seed( 'authors', diff --git a/test/has_one_test.dart b/test/has_one_test.dart index be3f2dc0..7169f545 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 CustomMapService()) - ..use('/books', new CustomMapService()); + ..use('/authors', new MapService()) + ..use('/books', new MapService()); await app.configure(seed( 'authors', @@ -41,6 +41,8 @@ main() { expect(author.keys, contains('book')); Map book = author['book']; + print('Author: $author'); + print('Book: $book'); expect(book['authorId'], equals(author['id'])); } });