diff --git a/packages/relations/.gitignore b/packages/relations/.gitignore new file mode 100644 index 00000000..e822a056 --- /dev/null +++ b/packages/relations/.gitignore @@ -0,0 +1,93 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +.scripts-bin/ +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 +### 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/packages/relations/.travis.yml b/packages/relations/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/packages/relations/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/packages/relations/LICENSE b/packages/relations/LICENSE new file mode 100644 index 00000000..89074fd3 --- /dev/null +++ b/packages/relations/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/packages/relations/README.md b/packages/relations/README.md new file mode 100644 index 00000000..ea8a4e74 --- /dev/null +++ b/packages/relations/README.md @@ -0,0 +1,25 @@ +# 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. + +```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')); +``` + +Supports: +* `hasOne` +* `hasMany` +* `hasManyThrough` +* `belongsTo` +* `belongsToMany` \ No newline at end of file diff --git a/packages/relations/lib/angel_relations.dart b/packages/relations/lib/angel_relations.dart new file mode 100644 index 00000000..5f2b6a7e --- /dev/null +++ b/packages/relations/lib/angel_relations.dart @@ -0,0 +1,9 @@ +/// Hooks to populate data returned from services, in a fashion +/// 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/packages/relations/lib/src/belongs_to.dart b/packages/relations/lib/src/belongs_to.dart new file mode 100644 index 00000000..68ae9c38 --- /dev/null +++ b/packages/relations/lib/src/belongs_to.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" +/// a single member of the service at [servicePath]. Use [as] to set the name +/// on the target object. +/// +/// Defaults: +/// * [localKey]: `userId` +/// * [foreignKey]: `id` +HookedServiceEventListener belongsTo(Pattern servicePath, + {String as, + String foreignKey, + 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); + 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.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/packages/relations/lib/src/belongs_to_many.dart b/packages/relations/lib/src/belongs_to_many.dart new file mode 100644 index 00000000..2b3a508a --- /dev/null +++ b/packages/relations/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/packages/relations/lib/src/has_many.dart b/packages/relations/lib/src/has_many.dart new file mode 100644 index 00000000..34cffcff --- /dev/null +++ b/packages/relations/lib/src/has_many.dart @@ -0,0 +1,71 @@ +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); + + _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 == null || indexed is! List || 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/packages/relations/lib/src/has_many_through.dart b/packages/relations/lib/src/has_many_through.dart new file mode 100644 index 00000000..9539519e --- /dev/null +++ b/packages/relations/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/packages/relations/lib/src/has_one.dart b/packages/relations/lib/src/has_one.dart new file mode 100644 index 00000000..8578081b --- /dev/null +++ b/packages/relations/lib/src/has_one.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" +/// 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); + + _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 == null || indexed is! List || 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/packages/relations/lib/src/no_service.dart b/packages/relations/lib/src/no_service.dart new file mode 100644 index 00000000..e9d78658 --- /dev/null +++ b/packages/relations/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/packages/relations/lib/src/plural.dart b/packages/relations/lib/src/plural.dart new file mode 100644 index 00000000..972d2fdd --- /dev/null +++ b/packages/relations/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/packages/relations/pubspec.yaml b/packages/relations/pubspec.yaml new file mode 100644 index 00000000..d25318d2 --- /dev/null +++ b/packages/relations/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.1 +environment: + sdk: ">=1.19.0" +dependencies: + angel_framework: ^1.0.0-dev +dev_dependencies: + angel_seeder: ^1.0.0 + test: ^0.12.0 diff --git a/packages/relations/test/belongs_to_test.dart b/packages/relations/test/belongs_to_test.dart new file mode 100644 index 00000000..d398e923 --- /dev/null +++ b/packages/relations/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/packages/relations/test/common.dart b/packages/relations/test/common.dart new file mode 100644 index 00000000..f8adf3dd --- /dev/null +++ b/packages/relations/test/common.dart @@ -0,0 +1,68 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:json_god/json_god.dart' as god; + +@deprecated +class CustomMapService 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/packages/relations/test/has_many_test.dart b/packages/relations/test/has_many_test.dart new file mode 100644 index 00000000..e27b19e4 --- /dev/null +++ b/packages/relations/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/packages/relations/test/has_one_test.dart b/packages/relations/test/has_one_test.dart new file mode 100644 index 00000000..7169f545 --- /dev/null +++ b/packages/relations/test/has_one_test.dart @@ -0,0 +1,59 @@ +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']; + print('Author: $author'); + print('Book: $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); + }); +}