This commit is contained in:
thosakwe 2017-01-29 21:39:11 -05:00
parent d8304b9104
commit 160c620f57
14 changed files with 535 additions and 0 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@
.packages
.project
.pub/
.scripts-bin/
build/
**/packages/

1
.travis.yml Normal file
View file

@ -0,0 +1 @@
language: dart

View file

@ -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`

7
lib/angel_relations.dart Normal file
View file

@ -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';

79
lib/src/belongs_to.dart Normal file
View file

@ -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);
};
}

74
lib/src/has_many.dart Normal file
View file

@ -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);
};
}

76
lib/src/has_one.dart Normal file
View file

@ -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);
};
}

2
lib/src/no_service.dart Normal file
View file

@ -0,0 +1,2 @@
ArgumentError noService(Pattern path) =>
new ArgumentError("No service exists at path '$path'.");

21
lib/src/plural.dart Normal file
View file

@ -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';
}

12
pubspec.yaml Normal file
View file

@ -0,0 +1,12 @@
author: "Tobe O <thosakwe@gmail.com>"
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

56
test/belongs_to_test.dart Normal file
View file

@ -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<Map>(
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);
});
}

67
test/common.dart Normal file
View file

@ -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<Map> _items = [];
Iterable<Map> tailor(Iterable<Map> 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});
}

61
test/has_many_test.dart Normal file
View file

@ -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<Map>(
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<Map> 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));
});
}

57
test/has_one_test.dart Normal file
View file

@ -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<Map>(
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);
});
}