2017-01-28 21:08:07 +00:00
|
|
|
/// Easy helper hooks.
|
|
|
|
library angel_framework.hooks;
|
|
|
|
|
2017-01-28 21:25:02 +00:00
|
|
|
import 'dart:async';
|
2017-01-29 19:49:28 +00:00
|
|
|
import 'dart:mirrors';
|
2017-01-28 21:08:07 +00:00
|
|
|
import 'package:json_god/json_god.dart' as god;
|
|
|
|
import 'angel_framework.dart';
|
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
/// Sequentially runs a set of [listeners].
|
|
|
|
HookedServiceEventListener chainListeners(
|
|
|
|
Iterable<HookedServiceEventListener> listeners) {
|
|
|
|
return (HookedServiceEvent e) async {
|
|
|
|
for (HookedServiceEventListener listener in listeners) await listener(e);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Runs a [callback] on every service, and listens for future services to run it again.
|
|
|
|
AngelConfigurer hookAllServices(callback(Service service)) {
|
|
|
|
return (Angel app) async {
|
|
|
|
List<Service> touched = [];
|
|
|
|
|
|
|
|
for (var service in app.services.values) {
|
|
|
|
if (!touched.contains(service)) {
|
|
|
|
await callback(service);
|
|
|
|
touched.add(service);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
app.onService.listen((service) {
|
|
|
|
if (!touched.contains(service)) return callback(service);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
/// Transforms `e.data` or `e.result` into JSON-friendly data, i.e. a Map. Runs on Iterables as well.
|
|
|
|
HookedServiceEventListener toJson() => transform(god.serializeObject);
|
2017-01-28 21:08:07 +00:00
|
|
|
|
2017-03-11 07:13:57 +00:00
|
|
|
/// Mutates `e.data` or `e.result` using the given [transformer].
|
2017-04-15 17:42:21 +00:00
|
|
|
///
|
|
|
|
/// You can optionally provide a [condition], which can be:
|
|
|
|
/// * A [Providers] instance, or String, to run only on certain clients
|
|
|
|
/// * The type [Providers], in which case the transformer will run on every client, but *not* on server-side events.
|
|
|
|
/// * A function: if the function returns `true` (sync or async, doesn't matter),
|
|
|
|
/// then the transformer will run. If not, the event will be skipped.
|
|
|
|
/// * An [Iterable] of the above three.
|
|
|
|
///
|
|
|
|
/// A provided function must take a [HookedServiceEvent] as its only parameter.
|
|
|
|
HookedServiceEventListener transform(transformer(obj), [condition]) {
|
|
|
|
Iterable cond = condition is Iterable ? condition : [condition];
|
|
|
|
|
|
|
|
_condition(HookedServiceEvent e, condition) async {
|
|
|
|
if (condition is Function)
|
|
|
|
return await condition(e);
|
|
|
|
else if (condition == Providers)
|
|
|
|
return true;
|
|
|
|
else {
|
|
|
|
if (e.params?.containsKey('provider') == true) {
|
|
|
|
var provider = e.params['provider'] as Providers;
|
|
|
|
if (condition is Providers)
|
|
|
|
return condition == provider;
|
|
|
|
else
|
|
|
|
return condition.toString() == provider.via;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
normalize(HookedServiceEvent e, obj) async {
|
|
|
|
bool transform = true;
|
|
|
|
|
|
|
|
for (var c in cond) {
|
|
|
|
var r = await _condition(e, c);
|
|
|
|
|
|
|
|
if (r != true) {
|
|
|
|
transform = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (transform != true) {
|
|
|
|
if (obj == null)
|
|
|
|
return null;
|
|
|
|
else if (obj is Iterable)
|
|
|
|
return obj.toList();
|
|
|
|
else
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
2017-03-11 07:17:52 +00:00
|
|
|
if (obj == null)
|
|
|
|
return null;
|
2017-04-15 17:42:21 +00:00
|
|
|
else if (obj is Iterable) {
|
|
|
|
var r = [];
|
|
|
|
|
|
|
|
for (var o in obj) {
|
|
|
|
r.add(await normalize(e, o));
|
|
|
|
}
|
|
|
|
|
|
|
|
return r;
|
|
|
|
} else
|
2017-03-11 07:17:52 +00:00
|
|
|
return transformer(obj);
|
|
|
|
}
|
|
|
|
|
2017-04-15 17:42:21 +00:00
|
|
|
return (HookedServiceEvent e) async {
|
2017-03-11 07:17:52 +00:00
|
|
|
if (e.isBefore) {
|
2017-04-15 17:42:21 +00:00
|
|
|
e.data = await normalize(e, e.data);
|
|
|
|
} else if (e.isAfter) e.result = await normalize(e, e.result);
|
2017-03-11 07:17:52 +00:00
|
|
|
};
|
2017-03-11 07:13:57 +00:00
|
|
|
}
|
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
/// Transforms `e.data` or `e.result` into an instance of the given [type],
|
2017-01-28 21:08:07 +00:00
|
|
|
/// if it is not already.
|
|
|
|
HookedServiceEventListener toType(Type type) {
|
|
|
|
return (HookedServiceEvent e) {
|
2017-01-29 19:49:28 +00:00
|
|
|
normalize(obj) {
|
|
|
|
if (obj != null && obj.runtimeType != type)
|
2017-02-13 00:38:33 +00:00
|
|
|
return god.deserializeDatum(obj, outputType: type);
|
|
|
|
return obj;
|
2017-01-29 19:49:28 +00:00
|
|
|
}
|
|
|
|
|
2017-02-13 00:38:33 +00:00
|
|
|
if (e.isBefore) {
|
2017-03-11 07:17:52 +00:00
|
|
|
e.data = normalize(e.data);
|
2017-02-13 00:38:33 +00:00
|
|
|
} else
|
|
|
|
e.result = normalize(e.result);
|
2017-01-28 21:08:07 +00:00
|
|
|
};
|
|
|
|
}
|
2017-01-28 21:25:02 +00:00
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
/// Removes one or more [key]s from `e.data` or `e.result`.
|
|
|
|
/// Works on single objects and iterables.
|
2017-03-06 04:55:13 +00:00
|
|
|
///
|
|
|
|
/// Only applies to the client-side.
|
2017-03-04 21:12:39 +00:00
|
|
|
HookedServiceEventListener remove(key, [remover(key, obj)]) {
|
2017-01-28 21:25:02 +00:00
|
|
|
return (HookedServiceEvent e) async {
|
|
|
|
_remover(key, obj) {
|
|
|
|
if (remover != null)
|
|
|
|
return remover(key, obj);
|
|
|
|
else if (obj is List)
|
|
|
|
return obj..remove(key);
|
|
|
|
else if (obj is Iterable)
|
|
|
|
return obj.where((k) => !key);
|
|
|
|
else if (obj is Map)
|
|
|
|
return obj..remove(key);
|
|
|
|
else if (obj is Extensible)
|
|
|
|
return obj..properties.remove(key);
|
2017-01-29 19:49:28 +00:00
|
|
|
else {
|
|
|
|
try {
|
|
|
|
reflect(obj).setField(new Symbol(key), null);
|
|
|
|
return obj;
|
|
|
|
} catch (e) {
|
2017-04-15 17:42:21 +00:00
|
|
|
throw new ArgumentError("Cannot remove key '$key' from $obj.");
|
2017-01-29 19:49:28 +00:00
|
|
|
}
|
|
|
|
}
|
2017-01-28 21:25:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var keys = key is Iterable ? key : [key];
|
|
|
|
|
|
|
|
_removeAll(obj) async {
|
|
|
|
var r = obj;
|
|
|
|
|
|
|
|
for (var key in keys) {
|
|
|
|
r = await _remover(key, r);
|
|
|
|
}
|
|
|
|
|
|
|
|
return r;
|
|
|
|
}
|
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
normalize(obj) async {
|
|
|
|
if (obj != null) {
|
|
|
|
if (obj is Iterable) {
|
2017-04-26 22:55:47 +00:00
|
|
|
return await Future.wait(obj.map(_removeAll));
|
2017-01-29 19:49:28 +00:00
|
|
|
} else
|
2017-04-26 22:55:47 +00:00
|
|
|
return await _removeAll(obj);
|
2017-01-29 19:49:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
if (e.params?.containsKey('provider') == true) {
|
|
|
|
if (e.isBefore) {
|
|
|
|
e.data = await normalize(e.data);
|
|
|
|
} else if (e.isAfter) {
|
|
|
|
e.result = await normalize(e.result);
|
|
|
|
}
|
|
|
|
}
|
2017-01-28 21:25:02 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
/// Disables a service method for client access from a provider.
|
2017-01-28 21:25:02 +00:00
|
|
|
///
|
|
|
|
/// [provider] can be either a String, [Providers], an Iterable of String, or a
|
|
|
|
/// function that takes a [HookedServiceEvent] and returns a bool.
|
|
|
|
/// Futures are allowed.
|
2017-01-29 19:49:28 +00:00
|
|
|
///
|
|
|
|
/// If [provider] is `null`, then it will be disabled to all clients.
|
2017-01-28 21:25:02 +00:00
|
|
|
HookedServiceEventListener disable([provider]) {
|
|
|
|
return (HookedServiceEvent e) async {
|
2017-01-29 19:49:28 +00:00
|
|
|
if (e.params.containsKey('provider')) {
|
|
|
|
if (provider == null)
|
|
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
|
|
else if (provider is Function) {
|
|
|
|
var r = await provider(e);
|
|
|
|
if (r != true) throw new AngelHttpException.methodNotAllowed();
|
|
|
|
} else {
|
|
|
|
_provide(p) => p is Providers ? p : new Providers(p.toString());
|
2017-01-28 21:25:02 +00:00
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
var providers = provider is Iterable
|
|
|
|
? provider.map(_provide)
|
|
|
|
: [_provide(provider)];
|
2017-01-28 21:25:02 +00:00
|
|
|
|
2017-01-29 19:49:28 +00:00
|
|
|
if (providers.any((Providers p) => p == e.params['provider'])) {
|
|
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
|
|
}
|
2017-01-28 21:25:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2017-03-28 23:29:22 +00:00
|
|
|
|
|
|
|
/// Serializes the current time to `e.data` or `e.result`.
|
|
|
|
/// You can provide an [assign] function to set the property on your object, and skip reflection.
|
|
|
|
///
|
|
|
|
/// Default key: `createdAt`
|
|
|
|
HookedServiceEventListener addCreatedAt({
|
|
|
|
assign(obj, String now),
|
|
|
|
String key,
|
|
|
|
}) {
|
|
|
|
var name = key?.isNotEmpty == true ? key : 'createdAt';
|
|
|
|
|
|
|
|
return (HookedServiceEvent e) async {
|
|
|
|
_assign(obj, String now) {
|
|
|
|
if (assign != null)
|
|
|
|
return assign(obj, now);
|
|
|
|
else if (obj is Map)
|
2017-04-29 04:18:45 +00:00
|
|
|
obj[name] = now;
|
2017-03-28 23:29:22 +00:00
|
|
|
else if (obj is Extensible)
|
2017-04-29 04:18:45 +00:00
|
|
|
obj..properties[name] = now;
|
2017-03-28 23:29:22 +00:00
|
|
|
else {
|
|
|
|
try {
|
|
|
|
reflect(obj).setField(new Symbol(name), now);
|
|
|
|
} catch (e) {
|
|
|
|
throw new ArgumentError("Cannot set key '$name' on $obj.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-25 02:32:16 +00:00
|
|
|
var now = new DateTime.now().toUtc().toIso8601String();
|
2017-03-28 23:29:22 +00:00
|
|
|
|
|
|
|
normalize(obj) async {
|
|
|
|
if (obj != null) {
|
|
|
|
if (obj is Iterable) {
|
|
|
|
obj.forEach(normalize);
|
|
|
|
} else {
|
|
|
|
await _assign(obj, now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.params?.containsKey('provider') == true)
|
|
|
|
await normalize(e.isBefore ? e.data : e.result);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-04-15 17:42:21 +00:00
|
|
|
/// Typo: Use [addUpdatedAt] instead.
|
|
|
|
@deprecated
|
|
|
|
HookedServiceEventListener addUpatedAt({
|
|
|
|
assign(obj, String now),
|
|
|
|
String key,
|
|
|
|
}) =>
|
|
|
|
addUpdatedAt(assign: assign, key: key);
|
|
|
|
|
2017-03-28 23:29:22 +00:00
|
|
|
/// Serializes the current time to `e.data` or `e.result`.
|
|
|
|
/// You can provide an [assign] function to set the property on your object, and skip reflection.///
|
|
|
|
/// Default key: `createdAt`
|
2017-04-15 17:42:21 +00:00
|
|
|
HookedServiceEventListener addUpdatedAt({
|
2017-03-28 23:29:22 +00:00
|
|
|
assign(obj, String now),
|
|
|
|
String key,
|
|
|
|
}) {
|
|
|
|
var name = key?.isNotEmpty == true ? key : 'updatedAt';
|
|
|
|
|
|
|
|
return (HookedServiceEvent e) async {
|
|
|
|
_assign(obj, String now) {
|
|
|
|
if (assign != null)
|
|
|
|
return assign(obj, now);
|
|
|
|
else if (obj is Map)
|
2017-04-29 04:18:45 +00:00
|
|
|
obj[name] = now;
|
2017-03-28 23:29:22 +00:00
|
|
|
else if (obj is Extensible)
|
2017-04-29 04:18:45 +00:00
|
|
|
obj..properties[name] = now;
|
2017-03-28 23:29:22 +00:00
|
|
|
else {
|
|
|
|
try {
|
|
|
|
reflect(obj).setField(new Symbol(name), now);
|
|
|
|
} catch (e) {
|
|
|
|
throw new ArgumentError("Cannot SET key '$name' ON $obj.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-25 02:32:16 +00:00
|
|
|
var now = new DateTime.now().toUtc().toIso8601String();
|
2017-03-28 23:29:22 +00:00
|
|
|
|
|
|
|
normalize(obj) async {
|
|
|
|
if (obj != null) {
|
|
|
|
if (obj is Iterable) {
|
|
|
|
obj.forEach(normalize);
|
|
|
|
} else {
|
|
|
|
await _assign(obj, now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.params?.containsKey('provider') == true)
|
|
|
|
await normalize(e.isBefore ? e.data : e.result);
|
|
|
|
};
|
|
|
|
}
|