Add 'packages/relations/' from commit 'f1d7081f462d497a1d5810356132867158379e79'
git-subtree-dir: packages/relations git-subtree-mainline:b1a6f262ea
git-subtree-split:f1d7081f46
This commit is contained in:
commit
cad3b9c746
17 changed files with 829 additions and 0 deletions
93
packages/relations/.gitignore
vendored
Normal file
93
packages/relations/.gitignore
vendored
Normal 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
|
1
packages/relations/.travis.yml
Normal file
1
packages/relations/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
language: dart
|
21
packages/relations/LICENSE
Normal file
21
packages/relations/LICENSE
Normal 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.
|
25
packages/relations/README.md
Normal file
25
packages/relations/README.md
Normal 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`
|
9
packages/relations/lib/angel_relations.dart
Normal file
9
packages/relations/lib/angel_relations.dart
Normal 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';
|
78
packages/relations/lib/src/belongs_to.dart
Normal file
78
packages/relations/lib/src/belongs_to.dart
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
78
packages/relations/lib/src/belongs_to_many.dart
Normal file
78
packages/relations/lib/src/belongs_to_many.dart
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
71
packages/relations/lib/src/has_many.dart
Normal file
71
packages/relations/lib/src/has_many.dart
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
100
packages/relations/lib/src/has_many_through.dart
Normal file
100
packages/relations/lib/src/has_many_through.dart
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
74
packages/relations/lib/src/has_one.dart
Normal file
74
packages/relations/lib/src/has_one.dart
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
2
packages/relations/lib/src/no_service.dart
Normal file
2
packages/relations/lib/src/no_service.dart
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ArgumentError noService(Pattern path) =>
|
||||||
|
new ArgumentError("No service exists at path '$path'.");
|
21
packages/relations/lib/src/plural.dart
Normal file
21
packages/relations/lib/src/plural.dart
Normal 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
packages/relations/pubspec.yaml
Normal file
12
packages/relations/pubspec.yaml
Normal 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
|
56
packages/relations/test/belongs_to_test.dart
Normal file
56
packages/relations/test/belongs_to_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
68
packages/relations/test/common.dart
Normal file
68
packages/relations/test/common.dart
Normal 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});
|
||||||
|
}
|
61
packages/relations/test/has_many_test.dart
Normal file
61
packages/relations/test/has_many_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
59
packages/relations/test/has_one_test.dart
Normal file
59
packages/relations/test/has_one_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue