Add 'packages/relations/' from commit 'f1d7081f462d497a1d5810356132867158379e79'

git-subtree-dir: packages/relations
git-subtree-mainline: b1a6f262ea
git-subtree-split: f1d7081f46
This commit is contained in:
Tobe O 2020-02-15 18:44:01 -05:00
commit cad3b9c746
17 changed files with 829 additions and 0 deletions

93
packages/relations/.gitignore vendored Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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"
/// 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);
};
}

View file

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

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

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.1
environment:
sdk: ">=1.19.0"
dependencies:
angel_framework: ^1.0.0-dev
dev_dependencies:
angel_seeder: ^1.0.0
test: ^0.12.0

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

View file

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

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

View file

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