344 lines
11 KiB
Dart
344 lines
11 KiB
Dart
library angel_framework.http.service;
|
|
|
|
import 'dart:async';
|
|
import 'package:angel_http_exception/angel_http_exception.dart';
|
|
import 'package:merge_map/merge_map.dart';
|
|
import 'package:quiver_hashcode/hashcode.dart';
|
|
import '../util.dart';
|
|
import 'anonymous_service.dart';
|
|
import 'hooked_service.dart' show HookedService;
|
|
import 'metadata.dart';
|
|
import 'response_context.dart';
|
|
import 'routable.dart';
|
|
import 'server.dart';
|
|
|
|
/// Indicates how the service was accessed.
|
|
///
|
|
/// This will be passed to the `params` object in a service method.
|
|
/// When requested on the server side, this will be null.
|
|
class Providers {
|
|
/// The transport through which the client is accessing this service.
|
|
final String via;
|
|
|
|
const Providers(String this.via);
|
|
|
|
static const String viaRest = "rest";
|
|
static const String viaWebsocket = "websocket";
|
|
static const String viaGraphQL = "graphql";
|
|
|
|
/// Represents a request via REST.
|
|
static const Providers rest = Providers(viaRest);
|
|
|
|
/// Represents a request over WebSockets.
|
|
static const Providers websocket = Providers(viaWebsocket);
|
|
|
|
/// Represents a request parsed from GraphQL.
|
|
static const Providers graphQL = Providers(viaGraphQL);
|
|
|
|
@override
|
|
int get hashCode => hashObjects([via]);
|
|
|
|
@override
|
|
bool operator ==(other) => other is Providers && other.via == via;
|
|
|
|
Map<String, String> toJson() {
|
|
return {'via': via};
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'via:$via';
|
|
}
|
|
}
|
|
|
|
/// A front-facing interface that can present data to and operate on data on behalf of the user.
|
|
///
|
|
/// Heavily inspired by FeathersJS. <3
|
|
class Service<Id, Data> extends Routable {
|
|
/// A [List] of keys that services should ignore, should they see them in the query.
|
|
static const List<String> specialQueryKeys = <String>[
|
|
r'$limit',
|
|
r'$sort',
|
|
'page',
|
|
'token'
|
|
];
|
|
|
|
/// Handlers that must run to ensure this service's functionality.
|
|
List<RequestHandler> get bootstrappers => [];
|
|
|
|
/// The [Angel] app powering this service.
|
|
Angel app;
|
|
|
|
/// Closes this service, including any database connections or stream controllers.
|
|
void close() {}
|
|
|
|
/// Retrieves the first object from the result of calling [index] with the given [params].
|
|
///
|
|
/// If the result of [index] is `null`, OR an empty [Iterable], a 404 `AngelHttpException` will be thrown.
|
|
///
|
|
/// If the result is both non-null and NOT an [Iterable], it will be returned as-is.
|
|
///
|
|
/// If the result is a non-empty [Iterable], [findOne] will return `it.first`, where `it` is the aforementioned [Iterable].
|
|
///
|
|
/// A custom [errorMessage] may be provided.
|
|
Future<Data> findOne(
|
|
[Map<String, dynamic> params,
|
|
String errorMessage = 'No record was found matching the given query.']) {
|
|
return index(params).then((result) {
|
|
if (result == null) {
|
|
throw new AngelHttpException.notFound(message: errorMessage);
|
|
} else {
|
|
if (result.isEmpty) {
|
|
throw new AngelHttpException.notFound(message: errorMessage);
|
|
} else {
|
|
return result.first;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Retrieves all resources.
|
|
Future<List<Data>> index([Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Retrieves the desired resource.
|
|
Future<Data> read(Id id, [Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Reads multiple resources at once.
|
|
///
|
|
/// Service implementations should override this to ensure data is fetched within a
|
|
/// single round trip.
|
|
Future<List<Data>> readMany(List<Id> ids, [Map<String, dynamic> params]) {
|
|
return Future.wait(ids.map((id) => read(id, params)));
|
|
}
|
|
|
|
/// Creates a resource.
|
|
Future<Data> create(Data data, [Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Modifies a resource.
|
|
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Overwrites a resource.
|
|
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Removes the given resource.
|
|
Future<Data> remove(Id id, [Map<String, dynamic> params]) {
|
|
throw new AngelHttpException.methodNotAllowed();
|
|
}
|
|
|
|
/// Creates an [AnonymousService] that wraps over this one, and maps input and output
|
|
/// using two converter functions.
|
|
///
|
|
/// Handy utility for handling data in a type-safe manner.
|
|
Service<Id, U> map<U>(U Function(Data) encoder, Data Function(U) decoder) {
|
|
return new AnonymousService<Id, U>(
|
|
index: ([params]) {
|
|
return index(params).then((it) => it.map(encoder).toList());
|
|
},
|
|
read: (id, [params]) {
|
|
return read(id, params).then(encoder);
|
|
},
|
|
create: (data, [params]) {
|
|
return create(decoder(data), params).then(encoder);
|
|
},
|
|
modify: (id, data, [params]) {
|
|
return modify(id, decoder(data), params).then(encoder);
|
|
},
|
|
update: (id, data, [params]) {
|
|
return update(id, decoder(data), params).then(encoder);
|
|
},
|
|
remove: (id, [params]) {
|
|
return remove(id, params).then(encoder);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Transforms an [id] (whether it is a String, num, etc.) into one acceptable by a service.
|
|
///
|
|
/// The single type argument, [T], is used to determine how to parse the [id].
|
|
///
|
|
/// For example, `parseId<bool>` attempts to parse the value as a [bool].
|
|
static T parseId<T>(id) {
|
|
if (id == 'null' || id == null)
|
|
return null;
|
|
else if (T == String)
|
|
return id.toString() as T;
|
|
else if (T == int)
|
|
return int.parse(id.toString()) as T;
|
|
else if (T == bool)
|
|
return (id == true || id?.toString() == 'true') as T;
|
|
else if (T == double)
|
|
return int.parse(id.toString()) as T;
|
|
else if (T == num)
|
|
return num.parse(id.toString()) as T;
|
|
else
|
|
return id as T;
|
|
}
|
|
|
|
/// Generates RESTful routes pointing to this class's methods.
|
|
void addRoutes([Service service]) {
|
|
_addRoutesInner(service ?? this, bootstrappers);
|
|
}
|
|
|
|
void _addRoutesInner(Service service, Iterable<RequestHandler> handlerss) {
|
|
var restProvider = {'provider': Providers.rest};
|
|
var handlers = new List<RequestHandler>.from(handlerss);
|
|
|
|
// Add global middleware if declared on the instance itself
|
|
Middleware before =
|
|
getAnnotation(service, Middleware, app.container.reflector);
|
|
|
|
if (before != null) handlers.addAll(before.handlers);
|
|
|
|
Middleware indexMiddleware =
|
|
getAnnotation(service.index, Middleware, app.container.reflector);
|
|
get('/', (req, res) {
|
|
return this.index(mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
},
|
|
middleware: <RequestHandler>[]
|
|
..addAll(handlers)
|
|
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
|
|
|
|
Middleware createMiddleware =
|
|
getAnnotation(service.create, Middleware, app.container.reflector);
|
|
post('/', (req, ResponseContext res) {
|
|
return req.parseBody().then((_) {
|
|
return this
|
|
.create(
|
|
req.bodyAsMap as Data,
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]))
|
|
.then((r) {
|
|
res.statusCode = 201;
|
|
return r;
|
|
});
|
|
});
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(createMiddleware == null) ? [] : createMiddleware.handlers));
|
|
|
|
Middleware readMiddleware =
|
|
getAnnotation(service.read, Middleware, app.container.reflector);
|
|
|
|
get('/:id', (req, res) {
|
|
return this.read(
|
|
parseId<Id>(req.params['id']),
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
|
|
|
|
Middleware modifyMiddleware =
|
|
getAnnotation(service.modify, Middleware, app.container.reflector);
|
|
patch('/:id', (req, res) {
|
|
return req.parseBody().then((_) {
|
|
return this.modify(
|
|
parseId<Id>(req.params['id']),
|
|
req.bodyAsMap as Data,
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
});
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
|
|
|
|
Middleware updateMiddleware =
|
|
getAnnotation(service.update, Middleware, app.container.reflector);
|
|
post('/:id', (req, res) {
|
|
return req.parseBody().then((_) {
|
|
return this.update(
|
|
parseId<Id>(req.params['id']),
|
|
req.bodyAsMap as Data,
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
});
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
|
put('/:id', (req, res) {
|
|
return req.parseBody().then((_) {
|
|
return this.update(
|
|
parseId<Id>(req.params['id']),
|
|
req.bodyAsMap as Data,
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
});
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
|
|
|
|
Middleware removeMiddleware =
|
|
getAnnotation(service.remove, Middleware, app.container.reflector);
|
|
delete('/', (req, res) {
|
|
return this.remove(
|
|
null,
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
|
delete('/:id', (req, res) {
|
|
return this.remove(
|
|
parseId<Id>(req.params['id']),
|
|
mergeMap([
|
|
{'query': req.queryParameters},
|
|
restProvider,
|
|
req.serviceParams
|
|
]));
|
|
},
|
|
middleware: []
|
|
..addAll(handlers)
|
|
..addAll(
|
|
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
|
|
|
|
// REST compliance
|
|
put('/', (req, res) => throw new AngelHttpException.notFound());
|
|
patch('/', (req, res) => throw new AngelHttpException.notFound());
|
|
}
|
|
|
|
/// Invoked when this service is wrapped within a [HookedService].
|
|
void onHooked(HookedService hookedService) {}
|
|
}
|