2.0.0
This commit is contained in:
parent
7652a998b7
commit
9d1b368d5b
13 changed files with 248 additions and 204 deletions
|
@ -1,3 +1,8 @@
|
|||
# 2.0.0
|
||||
* Deprecate `basePath` in favor of `baseUrl`.
|
||||
* `Angel` now extends `http.Client`.
|
||||
* Deprecate `auth_types`.
|
||||
|
||||
# 2.0.0-alpha.2
|
||||
* Make Service `index` always return `List<Data>`.
|
||||
* Add `Service.map`.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
exclude:
|
||||
- test/io_test.dart
|
||||
errors:
|
||||
unawaited_futures: ignore
|
21
example/main.dart
Normal file
21
example/main.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
|
||||
Future doSomething(Angel app) async {
|
||||
var userService = app
|
||||
.service<String, Map<String, dynamic>>('api/users')
|
||||
.map(User.fromMap, User.toMap);
|
||||
|
||||
var users = await userService.index();
|
||||
print('Name: ${users.first.name}');
|
||||
}
|
||||
|
||||
class User {
|
||||
final String name;
|
||||
|
||||
User({this.name});
|
||||
|
||||
static User fromMap(Map data) => User(name: data['name'] as String);
|
||||
|
||||
static Map<String, String> toMap(User user) => {'name': user.name};
|
||||
}
|
|
@ -4,11 +4,12 @@ library angel_client;
|
|||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/src/response.dart' as http;
|
||||
import 'package:http/http.dart' as http;
|
||||
export 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// A function that configures an [Angel] client in some way.
|
||||
typedef Future AngelConfigurer(Angel app);
|
||||
typedef FutureOr<void> AngelConfigurer(Angel app);
|
||||
|
||||
/// A function that deserializes data received from the server.
|
||||
///
|
||||
|
@ -17,61 +18,110 @@ typedef Future AngelConfigurer(Angel app);
|
|||
typedef T AngelDeserializer<T>(x);
|
||||
|
||||
/// Represents an Angel server that we are querying.
|
||||
abstract class Angel {
|
||||
abstract class Angel extends http.BaseClient {
|
||||
/// A mutable member. When this is set, it holds a JSON Web Token
|
||||
/// that is automatically attached to every request sent.
|
||||
///
|
||||
/// This is designed with `package:angel_auth` in mind.
|
||||
String authToken;
|
||||
String basePath;
|
||||
|
||||
Angel(String this.basePath);
|
||||
/// The root URL at which the target server.
|
||||
final Uri baseUrl;
|
||||
|
||||
Angel(baseUrl)
|
||||
: this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
|
||||
/// Prefer to use [baseUrl] instead.
|
||||
@deprecated
|
||||
String get basePath => baseUrl.toString();
|
||||
|
||||
/// Fired whenever a WebSocket is successfully authenticated.
|
||||
Stream<AngelAuthResult> get onAuthenticated;
|
||||
|
||||
/// Authenticates against the server.
|
||||
///
|
||||
/// This is designed with `package:angel_auth` in mind.
|
||||
///
|
||||
/// The [type] is appended to the [authEndpoint], ex. `local` becomes `/auth/local`.
|
||||
///
|
||||
/// The given [credentials] are sent to server as-is; the request body is sent as JSON.
|
||||
Future<AngelAuthResult> authenticate(
|
||||
{String type,
|
||||
{@required String type,
|
||||
credentials,
|
||||
String authEndpoint: '/auth',
|
||||
String reviveEndpoint: '/auth/token'});
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'});
|
||||
|
||||
/// Shorthand for authenticating via a JWT string.
|
||||
Future<AngelAuthResult> reviveJwt(String token,
|
||||
{String authEndpoint = '/auth'}) {
|
||||
return authenticate(
|
||||
type: 'token',
|
||||
credentials: {'token': token},
|
||||
authEndpoint: authEndpoint);
|
||||
}
|
||||
|
||||
/// Opens the [url] in a new window, and returns a [Stream] that will fire a JWT on successful authentication.
|
||||
Stream<String> authenticateViaPopup(String url, {String eventName: 'token'});
|
||||
Stream<String> authenticateViaPopup(String url, {String eventName = 'token'});
|
||||
|
||||
Future close();
|
||||
/// Disposes of any outstanding resources.
|
||||
Future<void> close();
|
||||
|
||||
/// Applies an [AngelConfigurer] to this instance.
|
||||
Future configure(AngelConfigurer configurer) async {
|
||||
Future<void> configure(AngelConfigurer configurer) async {
|
||||
await configurer(this);
|
||||
}
|
||||
|
||||
/// Logs the current user out of the application.
|
||||
Future logout();
|
||||
FutureOr<void> logout();
|
||||
|
||||
/// Creates a [Service] instance that queries a given path on the server.
|
||||
///
|
||||
/// This expects that there is an Angel `Service` mounted on the server.
|
||||
///
|
||||
/// In other words, all endpoints will return [Data], except for the root of
|
||||
/// [path], which returns a [List<Data>].
|
||||
///
|
||||
/// You can pass a custom [deserializer], which is typically necessary in cases where
|
||||
/// `dart:mirrors` does not exist.
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer});
|
||||
{@deprecated Type type, AngelDeserializer<Data> deserializer});
|
||||
|
||||
Future<http.Response> delete(String url, {Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> delete(url, {Map<String, String> headers});
|
||||
|
||||
Future<http.Response> get(String url, {Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> get(url, {Map<String, String> headers});
|
||||
|
||||
Future<http.Response> head(String url, {Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> head(url, {Map<String, String> headers});
|
||||
|
||||
Future<http.Response> patch(String url, {body, Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> patch(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
|
||||
Future<http.Response> post(String url, {body, Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> post(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
|
||||
Future<http.Response> put(String url, {body, Map<String, String> headers});
|
||||
@override
|
||||
Future<http.Response> put(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
}
|
||||
|
||||
/// Represents the result of authentication with an Angel server.
|
||||
class AngelAuthResult {
|
||||
String _token;
|
||||
final Map<String, dynamic> data = {};
|
||||
|
||||
/// The JSON Web token that was sent with this response.
|
||||
String get token => _token;
|
||||
|
||||
AngelAuthResult({String token, Map<String, dynamic> data: const {}}) {
|
||||
AngelAuthResult({String token, Map<String, dynamic> data = const {}}) {
|
||||
_token = token;
|
||||
this.data.addAll(data ?? {});
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a response from a [Map].
|
||||
factory AngelAuthResult.fromMap(Map data) {
|
||||
final result = new AngelAuthResult();
|
||||
|
||||
|
@ -81,12 +131,22 @@ class AngelAuthResult {
|
|||
if (data is Map)
|
||||
result.data.addAll((data['data'] as Map<String, dynamic>) ?? {});
|
||||
|
||||
if (result.token == null) {
|
||||
throw new FormatException(
|
||||
'The required "token" field was not present in the given data.');
|
||||
} else if (data['data'] is! Map) {
|
||||
throw new FormatException(
|
||||
'The required "data" field in the given data was not a map; instead, it was ${data['data']}.');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a response from a [String].
|
||||
factory AngelAuthResult.fromJson(String s) =>
|
||||
new AngelAuthResult.fromMap(json.decode(s) as Map);
|
||||
|
||||
/// Converts this instance into a JSON-friendly representation.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'token': token, 'data': data};
|
||||
}
|
||||
|
@ -225,7 +285,7 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
|
|||
|
||||
final List<StreamSubscription> _subs = [];
|
||||
|
||||
ServiceList(this.service, {this.idField, Equality<Data> equality})
|
||||
ServiceList(this.service, {this.idField = 'id', Equality<Data> equality})
|
||||
: super([]) {
|
||||
_equality = equality;
|
||||
_equality ??= new EqualityBy<Data, Id>((map) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const String local = 'local';
|
||||
|
||||
/// Use [local] instead.
|
||||
@deprecated
|
||||
const String LOCAL = local;
|
||||
library auth_types;
|
||||
|
||||
const String local = 'local', token = 'token';
|
||||
|
|
|
@ -7,25 +7,17 @@ import 'package:http/src/base_request.dart' as http;
|
|||
import 'package:http/src/request.dart' as http;
|
||||
import 'package:http/src/response.dart' as http;
|
||||
import 'package:http/src/streamed_response.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'angel_client.dart';
|
||||
|
||||
final RegExp straySlashes = new RegExp(r"(^/)|(/+$)");
|
||||
const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
|
||||
const Map<String, String> _writeHeaders = const {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
_buildQuery(Map<String, dynamic> params) {
|
||||
if (params == null || params.isEmpty || params['query'] is! Map) return "";
|
||||
|
||||
List<String> query = [];
|
||||
|
||||
params['query'].forEach((k, v) {
|
||||
query.add('$k=${Uri.encodeQueryComponent(v.toString())}');
|
||||
});
|
||||
|
||||
return '?' + query.join('&');
|
||||
Map<String, String> _buildQuery(Map<String, dynamic> params) {
|
||||
return params?.map((k, v) => new MapEntry(k, v.toString()));
|
||||
}
|
||||
|
||||
bool _invalid(http.Response response) =>
|
||||
|
@ -36,7 +28,7 @@ bool _invalid(http.Response response) =>
|
|||
AngelHttpException failure(http.Response response,
|
||||
{error, String message, StackTrace stack}) {
|
||||
try {
|
||||
final v = json.decode(response.body);
|
||||
var v = json.decode(response.body);
|
||||
|
||||
if (v is Map && (v['is_error'] == true) || v['isError'] == true) {
|
||||
return new AngelHttpException.fromMap(v as Map);
|
||||
|
@ -71,89 +63,48 @@ abstract class BaseAngelClient extends Angel {
|
|||
Future<AngelAuthResult> authenticate(
|
||||
{String type,
|
||||
credentials,
|
||||
String authEndpoint: '/auth',
|
||||
String reviveEndpoint: '/auth/token'}) async {
|
||||
if (type == null) {
|
||||
final url = '$basePath$reviveEndpoint';
|
||||
String token;
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'}) async {
|
||||
type ??= 'token';
|
||||
|
||||
if (credentials is String)
|
||||
token = credentials;
|
||||
else if (credentials is Map && credentials.containsKey('token'))
|
||||
token = credentials['token'].toString();
|
||||
var segments = baseUrl.pathSegments
|
||||
.followedBy(p.split(authEndpoint))
|
||||
.followedBy([type]);
|
||||
var url = baseUrl.replace(path: p.joinAll(segments));
|
||||
http.Response response;
|
||||
|
||||
if (token == null) {
|
||||
throw new ArgumentError(
|
||||
'If `type` is not set, a JWT is expected as the `credentials` argument.');
|
||||
}
|
||||
|
||||
final response = await client.post(url, headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $token'
|
||||
});
|
||||
|
||||
if (_invalid(response)) {
|
||||
throw failure(response);
|
||||
}
|
||||
|
||||
try {
|
||||
final v = json.decode(response.body);
|
||||
|
||||
if (v is! Map ||
|
||||
!(v as Map).containsKey('data') ||
|
||||
!(v as Map).containsKey('token')) {
|
||||
throw new AngelHttpException.notAuthenticated(
|
||||
message:
|
||||
"Auth endpoint '$url' did not return a proper response.");
|
||||
}
|
||||
|
||||
var r = new AngelAuthResult.fromMap(v as Map);
|
||||
_onAuthenticated.add(r);
|
||||
return r;
|
||||
} on AngelHttpException {
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
if (credentials != null) {
|
||||
response = await post(url,
|
||||
body: json.encode(credentials), headers: _writeHeaders);
|
||||
} else {
|
||||
final url = '$basePath$authEndpoint/$type';
|
||||
http.Response response;
|
||||
response = await post(url, headers: _writeHeaders);
|
||||
}
|
||||
|
||||
if (credentials != null) {
|
||||
response = await client.post(url,
|
||||
body: json.encode(credentials), headers: _writeHeaders);
|
||||
} else {
|
||||
response = await client.post(url, headers: _writeHeaders);
|
||||
if (_invalid(response)) {
|
||||
throw failure(response);
|
||||
}
|
||||
|
||||
try {
|
||||
var v = json.decode(response.body);
|
||||
|
||||
if (v is! Map ||
|
||||
!(v as Map).containsKey('data') ||
|
||||
!(v as Map).containsKey('token')) {
|
||||
throw new AngelHttpException.notAuthenticated(
|
||||
message: "Auth endpoint '$url' did not return a proper response.");
|
||||
}
|
||||
|
||||
if (_invalid(response)) {
|
||||
throw failure(response);
|
||||
}
|
||||
|
||||
try {
|
||||
final v = json.decode(response.body);
|
||||
|
||||
if (v is! Map ||
|
||||
!(v as Map).containsKey('data') ||
|
||||
!(v as Map).containsKey('token')) {
|
||||
throw new AngelHttpException.notAuthenticated(
|
||||
message:
|
||||
"Auth endpoint '$url' did not return a proper response.");
|
||||
}
|
||||
|
||||
var r = new AngelAuthResult.fromMap(v as Map);
|
||||
_onAuthenticated.add(r);
|
||||
return r;
|
||||
} on AngelHttpException {
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
var r = new AngelAuthResult.fromMap(v as Map);
|
||||
_onAuthenticated.add(r);
|
||||
return r;
|
||||
} on AngelHttpException {
|
||||
rethrow;
|
||||
} catch (e, st) {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future close() async {
|
||||
Future<void> close() async {
|
||||
client.close();
|
||||
_onAuthenticated.close();
|
||||
Future.wait(_services.map((s) => s.close())).then((_) {
|
||||
|
@ -161,10 +112,17 @@ abstract class BaseAngelClient extends Angel {
|
|||
});
|
||||
}
|
||||
|
||||
Future logout() async {
|
||||
Future<void> logout() async {
|
||||
authToken = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
if (authToken?.isNotEmpty == true)
|
||||
request.headers['authorization'] ??= 'Bearer $authToken';
|
||||
return client.send(request);
|
||||
}
|
||||
|
||||
/// Sends a non-streaming [Request] and returns a non-streaming [Response].
|
||||
Future<http.Response> sendUnstreamed(
|
||||
String method, url, Map<String, String> headers,
|
||||
|
@ -174,80 +132,77 @@ abstract class BaseAngelClient extends Angel {
|
|||
|
||||
if (headers != null) request.headers.addAll(headers);
|
||||
|
||||
if (authToken?.isNotEmpty == true)
|
||||
request.headers['Authorization'] = 'Bearer $authToken';
|
||||
|
||||
if (encoding != null) request.encoding = encoding;
|
||||
if (body != null) {
|
||||
if (body is String) {
|
||||
request.body = body;
|
||||
} else if (body is List) {
|
||||
request.bodyBytes = new List.from(body);
|
||||
} else if (body is Map) {
|
||||
request.bodyFields = new Map.from(body);
|
||||
} else if (body is List<int>) {
|
||||
request.bodyBytes = new List<int>.from(body);
|
||||
} else if (body is Map<String, String>) {
|
||||
request.bodyFields = new Map<String, String>.from(body);
|
||||
} else {
|
||||
throw new ArgumentError('Invalid request body "$body".');
|
||||
throw new ArgumentError.value(body, 'body',
|
||||
'must be a String, List<int>, or Map<String, String>.');
|
||||
}
|
||||
}
|
||||
|
||||
return http.Response.fromStream(await client.send(request));
|
||||
return http.Response.fromStream(await send(request));
|
||||
}
|
||||
|
||||
@override
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer}) {
|
||||
String uri = path.toString().replaceAll(straySlashes, "");
|
||||
var s = new BaseAngelService<Id, Data>(client, this, '$basePath/$uri',
|
||||
var url = baseUrl.replace(path: p.join(baseUrl.path, path));
|
||||
var s = new BaseAngelService<Id, Data>(client, this, url,
|
||||
deserializer: deserializer);
|
||||
_services.add(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
String _join(url) {
|
||||
final head = basePath.replaceAll(new RegExp(r'/+$'), '');
|
||||
final tail = url.replaceAll(straySlashes, '');
|
||||
return '$head/$tail';
|
||||
Uri _join(url) {
|
||||
var u = url is Uri ? url : Uri.parse(url.toString());
|
||||
if (u.hasScheme || u.hasAuthority) return u;
|
||||
return baseUrl.replace(path: p.join(baseUrl.path, u.path));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> delete(String url,
|
||||
{Map<String, String> headers}) async {
|
||||
Future<http.Response> delete(url, {Map<String, String> headers}) async {
|
||||
return sendUnstreamed('DELETE', _join(url), headers);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> get(String url, {Map<String, String> headers}) async {
|
||||
Future<http.Response> get(url, {Map<String, String> headers}) async {
|
||||
return sendUnstreamed('GET', _join(url), headers);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> head(String url, {Map<String, String> headers}) async {
|
||||
Future<http.Response> head(url, {Map<String, String> headers}) async {
|
||||
return sendUnstreamed('HEAD', _join(url), headers);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> patch(String url,
|
||||
{body, Map<String, String> headers}) async {
|
||||
return sendUnstreamed('PATCH', _join(url), headers, body);
|
||||
Future<http.Response> patch(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
return sendUnstreamed('PATCH', _join(url), headers, body, encoding);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> post(String url,
|
||||
{body, Map<String, String> headers}) async {
|
||||
return sendUnstreamed('POST', _join(url), headers, body);
|
||||
Future<http.Response> post(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
return sendUnstreamed('POST', _join(url), headers, body, encoding);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> put(String url,
|
||||
{body, Map<String, String> headers}) async {
|
||||
return sendUnstreamed('PUT', _join(url), headers, body);
|
||||
Future<http.Response> put(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
return sendUnstreamed('PUT', _join(url), headers, body, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
||||
@override
|
||||
final BaseAngelClient app;
|
||||
final String basePath;
|
||||
final Uri baseUrl;
|
||||
final http.BaseClient client;
|
||||
final AngelDeserializer<Data> deserializer;
|
||||
|
||||
|
@ -286,7 +241,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
_onRemoved.close();
|
||||
}
|
||||
|
||||
BaseAngelService(this.client, this.app, this.basePath, {this.deserializer});
|
||||
BaseAngelService(this.client, this.app, baseUrl, {this.deserializer})
|
||||
: this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
|
||||
/// Use [baseUrl] instead.
|
||||
@deprecated
|
||||
String get basePath => baseUrl.toString();
|
||||
|
||||
Data deserialize(x) {
|
||||
return deserializer != null ? deserializer(x) : x as Data;
|
||||
|
@ -306,8 +266,8 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<List<Data>> index([Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'GET', '$basePath${_buildQuery(params)}', _readHeaders);
|
||||
var url = baseUrl.replace(queryParameters: _buildQuery(params));
|
||||
var response = await app.sendUnstreamed('GET', url, _readHeaders);
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
@ -317,7 +277,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
throw failure(response);
|
||||
}
|
||||
|
||||
final v = json.decode(response.body) as List;
|
||||
var v = json.decode(response.body) as List;
|
||||
var r = v.map(deserialize).toList();
|
||||
_onIndexed.add(r);
|
||||
return r;
|
||||
|
@ -333,8 +293,11 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<Data> read(id, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'GET', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
||||
var response = await app.sendUnstreamed('GET', url, _readHeaders);
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
@ -359,8 +322,9 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<Data> create(data, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed('POST',
|
||||
'$basePath/${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
var url = baseUrl.replace(queryParameters: _buildQuery(params));
|
||||
var response =
|
||||
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
@ -385,8 +349,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<Data> modify(id, data, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed('PATCH',
|
||||
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
||||
var response =
|
||||
await app.sendUnstreamed('PATCH', url, _writeHeaders, makeBody(data));
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
@ -411,8 +379,12 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<Data> update(id, data, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed('POST',
|
||||
'$basePath/$id${_buildQuery(params)}', _writeHeaders, makeBody(data));
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
||||
var response =
|
||||
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
@ -437,8 +409,11 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
@override
|
||||
Future<Data> remove(id, [Map<String, dynamic> params]) async {
|
||||
final response = await app.sendUnstreamed(
|
||||
'DELETE', '$basePath/$id${_buildQuery(params)}', _readHeaders);
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
||||
var response = await app.sendUnstreamed('DELETE', url, _readHeaders);
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
|
|
|
@ -15,42 +15,31 @@ export 'angel_client.dart';
|
|||
class Rest extends BaseAngelClient {
|
||||
Rest(String basePath) : super(new http.BrowserClient(), basePath);
|
||||
|
||||
@override
|
||||
Future<AngelAuthResult> authenticate(
|
||||
{String type,
|
||||
credentials,
|
||||
String authEndpoint: '/auth',
|
||||
String reviveEndpoint: '/auth/token'}) async {
|
||||
if (type == null) {
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'}) async {
|
||||
if (type == null || type == 'token') {
|
||||
if (!window.localStorage.containsKey('token')) {
|
||||
throw new Exception(
|
||||
'Cannot revive token from localStorage - there is none.');
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await super.authenticate(
|
||||
type: null,
|
||||
credentials: {'token': json.decode(window.localStorage['token'])},
|
||||
reviveEndpoint: reviveEndpoint);
|
||||
window.localStorage['token'] = json.encode(authToken = result.token);
|
||||
window.localStorage['user'] = json.encode(result.data);
|
||||
return result;
|
||||
} catch (e, st) {
|
||||
throw new AngelHttpException(e,
|
||||
message: 'Failed to revive auth token.', stackTrace: st);
|
||||
}
|
||||
} else {
|
||||
final result = await super.authenticate(
|
||||
type: type, credentials: credentials, authEndpoint: authEndpoint);
|
||||
window.localStorage['token'] = json.encode(authToken = result.token);
|
||||
window.localStorage['user'] = json.encode(result.data);
|
||||
return result;
|
||||
var token = json.decode(window.localStorage['token']);
|
||||
credentials ??= {'token': token};
|
||||
}
|
||||
|
||||
final result = await super.authenticate(
|
||||
type: type, credentials: credentials, authEndpoint: authEndpoint);
|
||||
window.localStorage['token'] = json.encode(authToken = result.token);
|
||||
window.localStorage['user'] = json.encode(result.data);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName: 'token', String errorMessage}) {
|
||||
{String eventName = 'token', String errorMessage}) {
|
||||
var ctrl = new StreamController<String>();
|
||||
var wnd = window.open(url, 'angel_client_auth_popup');
|
||||
|
||||
|
|
|
@ -11,8 +11,9 @@ class Rest extends BaseAngelClient {
|
|||
Rest(String basePath) : super(new http.Client() as http.BaseClient, basePath);
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url, {String eventName: 'token'}) {
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw new UnimplementedError(
|
||||
'Opening popup windows is not supported in the `dart:io` client.');
|
||||
'Opening popup windows is not supported in the `flutter` client.');
|
||||
}
|
||||
}
|
||||
|
|
11
lib/io.dart
11
lib/io.dart
|
@ -4,6 +4,7 @@ library angel_client.cli;
|
|||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'angel_client.dart';
|
||||
import 'base_angel_client.dart';
|
||||
export 'angel_client.dart';
|
||||
|
@ -17,14 +18,15 @@ class Rest extends BaseAngelClient {
|
|||
@override
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer deserializer}) {
|
||||
String uri = path.replaceAll(straySlashes, "");
|
||||
var s = new RestService<Id, Data>(client, this, "$basePath/$uri", type);
|
||||
var url = baseUrl.replace(path: p.join(baseUrl.path, path));
|
||||
var s = new RestService<Id, Data>(client, this, url, type);
|
||||
_services.add(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url, {String eventName: 'token'}) {
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw new UnimplementedError(
|
||||
'Opening popup windows is not supported in the `dart:io` client.');
|
||||
}
|
||||
|
@ -41,8 +43,7 @@ class Rest extends BaseAngelClient {
|
|||
class RestService<Id, Data> extends BaseAngelService<Id, Data> {
|
||||
final Type type;
|
||||
|
||||
RestService(
|
||||
http.BaseClient client, BaseAngelClient app, String url, this.type)
|
||||
RestService(http.BaseClient client, BaseAngelClient app, url, this.type)
|
||||
: super(client, app, url);
|
||||
|
||||
@override
|
||||
|
|
11
pubspec.yaml
11
pubspec.yaml
|
@ -1,5 +1,5 @@
|
|||
name: angel_client
|
||||
version: 2.0.0-alpha.2
|
||||
version: 2.0.0
|
||||
description: Client library for the Angel framework.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_client
|
||||
|
@ -8,13 +8,14 @@ environment:
|
|||
dependencies:
|
||||
angel_http_exception: ^1.0.0
|
||||
collection: ^1.0.0
|
||||
http: ">=0.11.0 <2.0.0"
|
||||
http: ^0.12.0
|
||||
json_god: ">=2.0.0-beta <3.0.0"
|
||||
merge_map: ^1.0.0
|
||||
path: ^1.0.0
|
||||
dev_dependencies:
|
||||
angel_framework: ^2.0.0-alpha
|
||||
angel_model: ^1.0.0
|
||||
build_runner: ^0.10.0
|
||||
build_web_compilers: ^0.4.0
|
||||
build_runner: ^1.0.0
|
||||
build_web_compilers: ^1.0.0
|
||||
mock_request: ^1.0.0
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.0.0
|
||||
|
|
|
@ -33,7 +33,7 @@ main() {
|
|||
expect(app.client.spec.method, 'POST');
|
||||
expect(app.client.spec.headers['content-type'],
|
||||
startsWith('application/json'));
|
||||
expect(app.client.spec.path, '/api/todos/');
|
||||
expect(app.client.spec.path, '/api/todos');
|
||||
expect(await read(app.client.spec.request.finalize()), '{}');
|
||||
});
|
||||
|
||||
|
@ -63,10 +63,6 @@ main() {
|
|||
});
|
||||
|
||||
group('authentication', () {
|
||||
test('no type, no token throws', () async {
|
||||
expect(app.authenticate, throwsArgumentError);
|
||||
});
|
||||
|
||||
test('no type defaults to token', () async {
|
||||
await app.authenticate(credentials: '<jwt>');
|
||||
expect(app.client.spec.path, '/auth/token');
|
||||
|
@ -77,11 +73,6 @@ main() {
|
|||
expect(app.client.spec.path, '/auth/local');
|
||||
});
|
||||
|
||||
test('token sends headers', () async {
|
||||
await app.authenticate(credentials: '<jwt>');
|
||||
expect(app.client.spec.headers['authorization'], 'Bearer <jwt>');
|
||||
});
|
||||
|
||||
test('credentials send right body', () async {
|
||||
await app
|
||||
.authenticate(type: 'local', credentials: {'username': 'password'});
|
||||
|
|
|
@ -15,7 +15,7 @@ class MockAngel extends BaseAngelClient {
|
|||
MockAngel() : super(null, 'http://localhost:3000');
|
||||
|
||||
@override
|
||||
authenticateViaPopup(String url, {String eventName: 'token'}) {
|
||||
authenticateViaPopup(String url, {String eventName = 'token'}) {
|
||||
throw new UnsupportedError('Nope');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@ import 'package:angel_client/browser.dart';
|
|||
/// Dummy app to ensure client works with DDC.
|
||||
main() {
|
||||
var app = new Rest(window.location.origin);
|
||||
window.alert(app.basePath);
|
||||
window.alert(app.baseUrl.toString());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue