Add 'packages/client/' from commit '180edbc46a556f6d572c3b4ade4b396a31a1bc42'

git-subtree-dir: packages/client
git-subtree-mainline: ae0afd3408
git-subtree-split: 180edbc46a
This commit is contained in:
Tobe O 2020-02-15 18:28:35 -05:00
commit 998aa62303
27 changed files with 1597 additions and 0 deletions

4
packages/client/.babelrc Normal file
View file

@ -0,0 +1,4 @@
{
"presets": ["es2015"],
"plugins": ["add-module-exports"]
}

81
packages/client/.gitignore vendored Normal file
View file

@ -0,0 +1,81 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
.project
.pub/
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
.idea
lib/angel_client.js
*.sum
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
.dart_tool

View file

@ -0,0 +1,10 @@
.babelrc
.istanbul.yml
.travis.yml
.editorconfig
.idea/
src/
test/
!lib/
.github/
coverage

View file

@ -0,0 +1,4 @@
language: dart
dart:
- dev
- stable

View file

@ -0,0 +1,43 @@
# 2.0.2
* `_join` previously discarded quer parameters, etc.
* Allow any `Map<String, dynamic>` as body, not just `Map<String, String>`.
# 2.0.1
* Change `BaseAngelClient` constructor to accept `dynamic` instead of `String` for `baseUrl.
# 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`.
# 2.0.0-alpha.1
* Refactor `params` to `Map<String, dynamic>`.
# 2.0.0-alpha
* Depend on Dart 2.
* Depend on Angel 2.
* Remove `dart2_constant`.
# 1.2.0+2
* Code cleanup + housekeeping, update to `dart2_constant`, and
ensured build works with `2.0.0-dev.64.1`.
# 1.2.0+1
* Removed a type annotation in `authenticateViaPopup` to prevent breaking with DDC.
# 1.2.0
* `ServiceList` now uses `Equality` from `package:collection` to compare items.
* `Service`s will now add service errors to corresponding streams if there is a listener.
# 1.1.0+3
* `ServiceList` no longer ignores empty `index` events.
# 1.1.0+2
* Updated `ServiceList` to also fire on `index`.
# 1.1.0+1
* Added `ServiceList`.

21
packages/client/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 angel-dart
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.

106
packages/client/README.md Normal file
View file

@ -0,0 +1,106 @@
# angel_client
[![Pub](https://img.shields.io/pub/v/angel_client.svg)](https://pub.dartlang.org/packages/angel_client)
[![build status](https://travis-ci.org/angel-dart/client.svg)](https://travis-ci.org/angel-dart/client)
Client library for the Angel framework.
This library provides virtually the same API as an Angel server.
The client can run in the browser, in Flutter, or on the command-line.
In addition, the client supports `angel_auth` authentication.
# Usage
```dart
// Choose one or the other, depending on platform
import 'package:angel_client/io.dart';
import 'package:angel_client/browser.dart';
import 'package:angel_client/flutter.dart';
main() async {
Angel app = new Rest("http://localhost:3000");
}
```
You can call `service` to receive an instance of `Service`, which acts as a client to a
service on the server at the given path (say that five times fast!).
```dart
foo() async {
Service Todos = app.service("todos");
List<Map> todos = await Todos.index();
print(todos.length);
}
```
The CLI client also supports reflection via `json_god`. There is no need to work with Maps;
you can use the same class on the client and the server.
```dart
class Todo extends Model {
String text;
Todo({String this.text});
}
bar() async {
// By the next release, this will just be:
// app.service<Todo>("todos")
Service Todos = app.service("todos", type: Todo);
List<Todo> todos = await Todos.index();
print(todos.length);
}
```
Just like on the server, services support `index`, `read`, `create`, `modify`, `update` and
`remove`.
## Authentication
Local auth:
```dart
var auth = await app.authenticate(type: 'local', credentials: {username: ..., password: ...});
print(auth.token);
print(auth.data); // User object
```
Revive an existing jwt:
```dart
Future<AngelAuthResult> reviveJwt(String jwt) {
return app.authenticate(credentials: {'token': jwt});
}
```
Via Popup:
```dart
app.authenticateViaPopup('/auth/google').listen((jwt) {
// Do something with the JWT
});
```
Resume a session from localStorage (browser only):
```dart
// Automatically checks for JSON-encoded 'token' in localStorage,
// and tries to revive it
await app.authenticate();
```
Logout:
```dart
await app.logout();
```
# Live Updates
Oftentimes, you will want to update a collection based on updates from a service.
Use `ServiceList` for this case:
```dart
build(BuildContext context) async {
var list = new ServiceList(app.service('api/todos'));
return new StreamBuilder(
stream: list.onChange,
builder: _yourBuildFunction,
);
}
```

View file

@ -0,0 +1,6 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
# errors:
# unawaited_futures: ignore

View file

@ -0,0 +1,14 @@
targets:
$default:
builders:
build_web_compilers|entrypoint:
generate_for:
- web/**.dart
options:
dart2js_args:
- --dump-info
- --fast-startup
- --minify
- --trust-type-annotations
- --trust-primitives
- --no-source-maps

View file

@ -0,0 +1 @@
# platforms: [vm, content-shell]

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

View file

@ -0,0 +1,347 @@
/// Client library for the Angel framework.
library angel_client;
import 'dart:async';
import 'package:collection/collection.dart';
import 'dart:convert';
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 FutureOr<void> AngelConfigurer(Angel app);
/// A function that deserializes data received from the server.
///
/// This is only really necessary in the browser, where `json_god`
/// doesn't work.
typedef T AngelDeserializer<T>(x);
/// Represents an Angel server that we are querying.
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;
/// 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(
{@required String type,
credentials,
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'});
/// Disposes of any outstanding resources.
Future<void> close();
/// Applies an [AngelConfigurer] to this instance.
Future<void> configure(AngelConfigurer configurer) async {
await configurer(this);
}
/// Logs the current user out of the application.
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,
{@deprecated Type type, AngelDeserializer<Data> deserializer});
@override
Future<http.Response> delete(url, {Map<String, String> headers});
@override
Future<http.Response> get(url, {Map<String, String> headers});
@override
Future<http.Response> head(url, {Map<String, String> headers});
@override
Future<http.Response> patch(url,
{body, Map<String, String> headers, Encoding encoding});
@override
Future<http.Response> post(url,
{body, Map<String, String> headers, Encoding encoding});
@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 {}}) {
_token = token;
this.data.addAll(data ?? {});
}
/// Attempts to deserialize a response from a [Map].
factory AngelAuthResult.fromMap(Map data) {
final result = new AngelAuthResult();
if (data is Map && data.containsKey('token') && data['token'] is String)
result._token = data['token'].toString();
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};
}
}
/// Queries a service on an Angel server, with the same API.
abstract class Service<Id, Data> {
/// Fired on `indexed` events.
Stream<List<Data>> get onIndexed;
/// Fired on `read` events.
Stream<Data> get onRead;
/// Fired on `created` events.
Stream<Data> get onCreated;
/// Fired on `modified` events.
Stream<Data> get onModified;
/// Fired on `updated` events.
Stream<Data> get onUpdated;
/// Fired on `removed` events.
Stream<Data> get onRemoved;
/// The Angel instance powering this service.
Angel get app;
Future close();
/// Retrieves all resources.
Future<List<Data>> index([Map<String, dynamic> params]);
/// Retrieves the desired resource.
Future<Data> read(Id id, [Map<String, dynamic> params]);
/// Creates a resource.
Future<Data> create(Data data, [Map<String, dynamic> params]);
/// Modifies a resource.
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]);
/// Overwrites a resource.
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]);
/// Removes the given resource.
Future<Data> remove(Id id, [Map<String, dynamic> params]);
/// Creates a [Service] 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 _MappedService(this, encoder, decoder);
}
}
class _MappedService<Id, Data, U> extends Service<Id, U> {
final Service<Id, Data> inner;
final U Function(Data) encoder;
final Data Function(U) decoder;
_MappedService(this.inner, this.encoder, this.decoder);
@override
Angel get app => inner.app;
@override
Future close() => new Future.value();
@override
Future<U> create(U data, [Map<String, dynamic> params]) {
return inner.create(decoder(data)).then(encoder);
}
@override
Future<List<U>> index([Map<String, dynamic> params]) {
return inner.index(params).then((l) => l.map(encoder).toList());
}
@override
Future<U> modify(Id id, U data, [Map<String, dynamic> params]) {
return inner.modify(id, decoder(data), params).then(encoder);
}
@override
Stream<U> get onCreated => inner.onCreated.map(encoder);
@override
Stream<List<U>> get onIndexed =>
inner.onIndexed.map((l) => l.map(encoder).toList());
@override
Stream<U> get onModified => inner.onModified.map(encoder);
@override
Stream<U> get onRead => inner.onRead.map(encoder);
@override
Stream<U> get onRemoved => inner.onRemoved.map(encoder);
@override
Stream<U> get onUpdated => inner.onUpdated.map(encoder);
@override
Future<U> read(Id id, [Map<String, dynamic> params]) {
return inner.read(id, params).then(encoder);
}
@override
Future<U> remove(Id id, [Map<String, dynamic> params]) {
return inner.remove(id, params).then(encoder);
}
@override
Future<U> update(Id id, U data, [Map<String, dynamic> params]) {
return inner.update(id, decoder(data), params).then(encoder);
}
}
/// A [List] that automatically updates itself whenever the referenced [service] fires an event.
class ServiceList<Id, Data> extends DelegatingList<Data> {
/// A field name used to compare [Map] by ID.
final String idField;
/// A function used to compare the ID's two items for equality.
///
/// Defaults to comparing the [idField] of `Map` instances.
Equality<Data> get equality => _equality;
Equality<Data> _equality;
final Service<Id, Data> service;
final StreamController<ServiceList<Id, Data>> _onChange =
new StreamController();
final List<StreamSubscription> _subs = [];
ServiceList(this.service, {this.idField = 'id', Equality<Data> equality})
: super([]) {
_equality = equality;
_equality ??= new EqualityBy<Data, Id>((map) {
if (map is Map)
return map[idField ?? 'id'] as Id;
else
throw new UnsupportedError(
'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.');
});
// Index
_subs.add(service.onIndexed.where(_notNull).listen((data) {
this
..clear()
..addAll(data);
_onChange.add(this);
}));
// Created
_subs.add(service.onCreated.where(_notNull).listen((item) {
add(item);
_onChange.add(this);
}));
// Modified/Updated
handleModified(Data item) {
var indices = <int>[];
for (int i = 0; i < length; i++) {
if (_equality.equals(item, this[i])) indices.add(i);
}
if (indices.isNotEmpty) {
for (var i in indices) this[i] = item;
_onChange.add(this);
}
}
_subs.addAll([
service.onModified.where(_notNull).listen(handleModified),
service.onUpdated.where(_notNull).listen(handleModified),
]);
// Removed
_subs.add(service.onRemoved.where(_notNull).listen((item) {
removeWhere((x) => _equality.equals(item, x));
_onChange.add(this);
}));
}
static bool _notNull(x) => x != null;
/// Fires whenever the underlying [service] fires a change event.
Stream<ServiceList<Id, Data>> get onChange => _onChange.stream;
Future close() async {
await _onChange.close();
}
}

View file

@ -0,0 +1,4 @@
@deprecated
library auth_types;
const String local = 'local', token = 'token';

View file

@ -0,0 +1,439 @@
import 'dart:async';
import 'dart:convert' show Encoding;
import 'package:angel_http_exception/angel_http_exception.dart';
import 'dart:convert';
import 'package:http/src/base_client.dart' as http;
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';
const Map<String, String> _readHeaders = const {'Accept': 'application/json'};
const Map<String, String> _writeHeaders = const {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
Map<String, String> _buildQuery(Map<String, dynamic> params) {
return params?.map((k, v) => new MapEntry(k, v.toString()));
}
bool _invalid(http.Response response) =>
response.statusCode == null ||
response.statusCode < 200 ||
response.statusCode >= 300;
AngelHttpException failure(http.Response response,
{error, String message, StackTrace stack}) {
try {
var v = json.decode(response.body);
if (v is Map && (v['is_error'] == true) || v['isError'] == true) {
return new AngelHttpException.fromMap(v as Map);
} else {
return new AngelHttpException(error,
message: message ??
'Unhandled exception while connecting to Angel backend.',
statusCode: response.statusCode,
stackTrace: stack);
}
} catch (e, st) {
return new AngelHttpException(error ?? e,
message: message ??
'Angel backend did not return JSON - an error likely occurred.',
statusCode: response.statusCode,
stackTrace: stack ?? st);
}
}
abstract class BaseAngelClient extends Angel {
final StreamController<AngelAuthResult> _onAuthenticated =
new StreamController<AngelAuthResult>();
final List<Service> _services = [];
final http.BaseClient client;
@override
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
BaseAngelClient(this.client, baseUrl) : super(baseUrl);
@override
Future<AngelAuthResult> authenticate(
{String type,
credentials,
String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}) async {
type ??= 'token';
var segments = baseUrl.pathSegments
.followedBy(p.split(authEndpoint))
.followedBy([type]);
var url = baseUrl.replace(path: p.joinAll(segments));
http.Response response;
if (credentials != null) {
response = await post(url,
body: json.encode(credentials), headers: _writeHeaders);
} else {
response = await 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.");
}
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<void> close() async {
client.close();
await _onAuthenticated.close();
await Future.wait(_services.map((s) => s.close())).then((_) {
_services.clear();
});
}
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,
[body, Encoding encoding]) async {
var request =
new http.Request(method, url is Uri ? url : Uri.parse(url.toString()));
if (headers != null) request.headers.addAll(headers);
if (encoding != null) request.encoding = encoding;
if (body != null) {
if (body is String) {
request.body = body;
} else if (body is List<int>) {
request.bodyBytes = new List<int>.from(body);
} else if (body is Map<String, dynamic>) {
request.bodyFields =
body.map((k, v) => MapEntry(k, v is String ? v : v.toString()));
} else {
throw new ArgumentError.value(body, 'body',
'must be a String, List<int>, or Map<String, String>.');
}
}
return http.Response.fromStream(await send(request));
}
@override
Service<Id, Data> service<Id, Data>(String path,
{Type type, AngelDeserializer<Data> deserializer}) {
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;
}
Uri _join(url) {
var u = url is Uri ? url : Uri.parse(url.toString());
if (u.hasScheme || u.hasAuthority) return u;
return u.replace(path: p.join(baseUrl.path, u.path));
}
@override
Future<http.Response> delete(url, {Map<String, String> headers}) async {
return sendUnstreamed('DELETE', _join(url), headers);
}
@override
Future<http.Response> get(url, {Map<String, String> headers}) async {
return sendUnstreamed('GET', _join(url), headers);
}
@override
Future<http.Response> head(url, {Map<String, String> headers}) async {
return sendUnstreamed('HEAD', _join(url), headers);
}
@override
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(url,
{body, Map<String, String> headers, Encoding encoding}) async {
return sendUnstreamed('POST', _join(url), headers, body, encoding);
}
@override
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 Uri baseUrl;
final http.BaseClient client;
final AngelDeserializer<Data> deserializer;
final StreamController<List<Data>> _onIndexed = new StreamController();
final StreamController<Data> _onRead = new StreamController(),
_onCreated = new StreamController(),
_onModified = new StreamController(),
_onUpdated = new StreamController(),
_onRemoved = new StreamController();
@override
Stream<List<Data>> get onIndexed => _onIndexed.stream;
@override
Stream<Data> get onRead => _onRead.stream;
@override
Stream<Data> get onCreated => _onCreated.stream;
@override
Stream<Data> get onModified => _onModified.stream;
@override
Stream<Data> get onUpdated => _onUpdated.stream;
@override
Stream<Data> get onRemoved => _onRemoved.stream;
@override
Future close() async {
await _onIndexed.close();
await _onRead.close();
await _onCreated.close();
await _onModified.close();
await _onUpdated.close();
await _onRemoved.close();
}
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;
}
makeBody(x) {
return json.encode(x);
}
Future<http.StreamedResponse> send(http.BaseRequest request) {
if (app.authToken != null && app.authToken.isNotEmpty) {
request.headers['Authorization'] = 'Bearer ${app.authToken}';
}
return client.send(request);
}
@override
Future<List<Data>> index([Map<String, dynamic> params]) async {
var url = baseUrl.replace(queryParameters: _buildQuery(params));
var response = await app.sendUnstreamed('GET', url, _readHeaders);
try {
if (_invalid(response)) {
if (_onIndexed.hasListener)
_onIndexed.addError(failure(response));
else
throw failure(response);
}
var v = json.decode(response.body) as List;
var r = v.map(deserialize).toList();
_onIndexed.add(r);
return r;
} catch (e, st) {
if (_onIndexed.hasListener)
_onIndexed.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
@override
Future<Data> read(id, [Map<String, dynamic> params]) async {
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)) {
if (_onRead.hasListener)
_onRead.addError(failure(response));
else
throw failure(response);
}
var r = deserialize(json.decode(response.body));
_onRead.add(r);
return r;
} catch (e, st) {
if (_onRead.hasListener)
_onRead.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
@override
Future<Data> create(data, [Map<String, dynamic> params]) async {
var url = baseUrl.replace(queryParameters: _buildQuery(params));
var response =
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
try {
if (_invalid(response)) {
if (_onCreated.hasListener)
_onCreated.addError(failure(response));
else
throw failure(response);
}
var r = deserialize(json.decode(response.body));
_onCreated.add(r);
return r;
} catch (e, st) {
if (_onCreated.hasListener)
_onCreated.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
@override
Future<Data> modify(id, data, [Map<String, dynamic> params]) async {
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)) {
if (_onModified.hasListener)
_onModified.addError(failure(response));
else
throw failure(response);
}
var r = deserialize(json.decode(response.body));
_onModified.add(r);
return r;
} catch (e, st) {
if (_onModified.hasListener)
_onModified.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
@override
Future<Data> update(id, data, [Map<String, dynamic> params]) async {
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)) {
if (_onUpdated.hasListener)
_onUpdated.addError(failure(response));
else
throw failure(response);
}
var r = deserialize(json.decode(response.body));
_onUpdated.add(r);
return r;
} catch (e, st) {
if (_onUpdated.hasListener)
_onUpdated.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
@override
Future<Data> remove(id, [Map<String, dynamic> params]) async {
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)) {
if (_onRemoved.hasListener)
_onRemoved.addError(failure(response));
else
throw failure(response);
}
var r = deserialize(json.decode(response.body));
_onRemoved.add(r);
return r;
} catch (e, st) {
if (_onRemoved.hasListener)
_onRemoved.addError(e, st);
else
throw failure(response, error: e, stack: st);
}
return null;
}
}

View file

@ -0,0 +1,80 @@
/// Browser client library for the Angel framework.
library angel_client.browser;
import 'dart:async'
show Future, Stream, StreamController, StreamSubscription, Timer;
import 'dart:html' show CustomEvent, Event, window;
import 'dart:convert';
import 'package:http/browser_client.dart' as http;
import 'angel_client.dart';
// import 'auth_types.dart' as auth_types;
import 'base_angel_client.dart';
export 'angel_client.dart';
/// Queries an Angel server via REST.
class Rest extends BaseAngelClient {
Rest(String basePath) : super(new http.BrowserClient(), basePath);
Future<AngelAuthResult> authenticate(
{String type,
credentials,
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.');
}
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}) {
var ctrl = new StreamController<String>();
var wnd = window.open(url, 'angel_client_auth_popup');
Timer t;
StreamSubscription sub;
t = new Timer.periodic(new Duration(milliseconds: 500), (timer) {
if (!ctrl.isClosed) {
if (wnd.closed) {
ctrl.addError(new AngelHttpException.notAuthenticated(
message:
errorMessage ?? 'Authentication via popup window failed.'));
ctrl.close();
timer.cancel();
sub?.cancel();
}
} else
timer.cancel();
});
sub = window.on[eventName ?? 'token'].listen((Event ev) {
var e = ev as CustomEvent;
if (!ctrl.isClosed) {
ctrl.add(e.detail.toString());
t.cancel();
ctrl.close();
sub.cancel();
}
});
return ctrl.stream;
}
@override
Future logout() {
window.localStorage.remove('token');
return super.logout();
}
}

View file

@ -0,0 +1,19 @@
/// Flutter-compatible client library for the Angel framework.
library angel_client.flutter;
import 'dart:async';
import 'package:http/http.dart' as http;
import 'base_angel_client.dart';
export 'angel_client.dart';
/// Queries an Angel server via REST.
class Rest extends BaseAngelClient {
Rest(String basePath) : super(new http.Client() as http.BaseClient, basePath);
@override
Stream<String> authenticateViaPopup(String url,
{String eventName = 'token'}) {
throw new UnimplementedError(
'Opening popup windows is not supported in the `flutter` client.');
}
}

View file

@ -0,0 +1,68 @@
/// Command-line client library for the Angel framework.
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';
/// Queries an Angel server via REST.
class Rest extends BaseAngelClient {
final List<Service> _services = [];
Rest(String path) : super(new http.Client() as http.BaseClient, path);
@override
Service<Id, Data> service<Id, Data>(String path,
{Type type, AngelDeserializer deserializer}) {
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'}) {
throw new UnimplementedError(
'Opening popup windows is not supported in the `dart:io` client.');
}
Future close() async {
await super.close();
await Future.wait(_services.map((s) => s.close())).then((_) {
_services.clear();
});
}
}
/// Queries an Angel service via REST.
class RestService<Id, Data> extends BaseAngelService<Id, Data> {
final Type type;
RestService(http.BaseClient client, BaseAngelClient app, url, this.type)
: super(client, app, url);
@override
Data deserialize(x) {
if (type != null) {
return x.runtimeType == type
? x as Data
: god.deserializeDatum(x, outputType: type) as Data;
}
return x as Data;
}
@override
makeBody(x) {
if (type != null) {
return super.makeBody(god.serializeObject(x));
}
return super.makeBody(x);
}
}

View file

@ -0,0 +1,35 @@
{
"name": "angel_client",
"version": "1.0.0-dev",
"description": "Client library for the Angel framework.",
"main": "build/angel_client.js",
"jsnext:main": "lib/angel_client.js",
"directories": {
"test": "test"
},
"scripts": {
"compile": "npm run dartdevc && babel -o build/angel_client.js lib/angel_client.js",
"dartdevc": "dartdevc --modules node -o lib/angel_client.js lib/angel_client.dart",
"prepublish": "npm run compile",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/angel-dart/angel_client.git"
},
"keywords": [
"angel",
"angel_client"
],
"author": "Tobe O <thosakwe@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/angel-dart/angel_client/issues"
},
"homepage": "https://github.com/angel-dart/angel_client#readme",
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-preset-es2015": "^6.18.0"
}
}

View file

@ -0,0 +1,23 @@
name: angel_client
version: 2.0.2
description: Support for querying Angel servers in the browser, Flutter, and command-line.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_client
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_http_exception: ^1.0.0
collection: ^1.0.0
http: ^0.12.0
json_god: ">=2.0.0-beta <3.0.0"
meta: ^1.0.0
path: ^1.0.0
dev_dependencies:
angel_framework: ^2.0.0-alpha
angel_model: ^1.0.0
async: ^2.0.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

BIN
packages/client/test/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,85 @@
import 'package:angel_client/angel_client.dart';
import 'dart:convert';
import 'package:test/test.dart';
import 'common.dart';
main() {
var app = new MockAngel();
Service todoService = app.service('api/todos');
test('sets method,body,headers,path', () async {
await app.post('/post', headers: {'method': 'post'}, body: 'post');
expect(app.client.spec.method, 'POST');
expect(app.client.spec.path, '/post');
expect(app.client.spec.headers['method'], 'post');
expect(await read(app.client.spec.request.finalize()), 'post');
});
group('service methods', () {
test('index', () async {
await todoService.index();
expect(app.client.spec.method, 'GET');
expect(app.client.spec.path, '/api/todos');
});
test('read', () async {
await todoService.read('sleep');
expect(app.client.spec.method, 'GET');
expect(app.client.spec.path, '/api/todos/sleep');
});
test('create', () async {
await todoService.create({});
expect(app.client.spec.method, 'POST');
expect(app.client.spec.headers['content-type'],
startsWith('application/json'));
expect(app.client.spec.path, '/api/todos');
expect(await read(app.client.spec.request.finalize()), '{}');
});
test('modify', () async {
await todoService.modify('sleep', {});
expect(app.client.spec.method, 'PATCH');
expect(app.client.spec.headers['content-type'],
startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}');
});
test('update', () async {
await todoService.update('sleep', {});
expect(app.client.spec.method, 'POST');
expect(app.client.spec.headers['content-type'],
startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}');
});
test('remove', () async {
await todoService.remove('sleep');
expect(app.client.spec.method, 'DELETE');
expect(app.client.spec.path, '/api/todos/sleep');
});
});
group('authentication', () {
test('no type defaults to token', () async {
await app.authenticate(credentials: '<jwt>');
expect(app.client.spec.path, '/auth/token');
});
test('sets type', () async {
await app.authenticate(type: 'local');
expect(app.client.spec.path, '/auth/local');
});
test('credentials send right body', () async {
await app
.authenticate(type: 'local', credentials: {'username': 'password'});
expect(
await read(app.client.spec.request.finalize()),
json.encode({'username': 'password'}),
);
});
});
}

View file

@ -0,0 +1,70 @@
import 'dart:async';
import 'package:angel_client/base_angel_client.dart';
import 'dart:convert';
import 'package:http/src/base_client.dart' as http;
import 'package:http/src/base_request.dart' as http;
import 'package:http/src/streamed_response.dart' as http;
Future<String> read(Stream<List<int>> stream) =>
stream.transform(utf8.decoder).join();
class MockAngel extends BaseAngelClient {
@override
final SpecClient client = new SpecClient();
MockAngel() : super(null, 'http://localhost:3000');
@override
authenticateViaPopup(String url, {String eventName = 'token'}) {
throw new UnsupportedError('Nope');
}
}
class SpecClient extends http.BaseClient {
Spec _spec;
Spec get spec => _spec;
@override
send(http.BaseRequest request) {
_spec = new Spec(request, request.method, request.url.path, request.headers,
request.contentLength);
dynamic data = {'text': 'Clean your room!', 'completed': true};
if (request.url.path.contains('auth')) {
data = {
'token': '<jwt>',
'data': {'username': 'password'}
};
} else if (request.url.path == '/api/todos' && request.method == 'GET') {
data = [data];
}
return new Future<http.StreamedResponse>.value(new http.StreamedResponse(
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
200,
headers: {
'content-type': 'application/json',
},
));
}
}
class Spec {
final http.BaseRequest request;
final String method, path;
final Map<String, String> headers;
final int contentLength;
Spec(this.request, this.method, this.path, this.headers, this.contentLength);
@override
String toString() {
return {
'method': method,
'path': path,
'headers': headers,
'content_length': contentLength,
}.toString();
}
}

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Browser</title>
</head>
<body>
<script type="application/dart" src="browser.dart"></script>
</body>
</html>

View file

@ -0,0 +1,61 @@
import 'package:async/async.dart';
import 'dart:io';
import 'package:angel_client/io.dart' as c;
import 'package:angel_framework/angel_framework.dart' as s;
import 'package:angel_framework/http.dart' as s;
import 'package:pedantic/pedantic.dart';
import 'package:test/test.dart';
main() {
HttpServer server;
c.Angel app;
c.ServiceList list;
StreamQueue queue;
setUp(() async {
var serverApp = new s.Angel();
var http = new s.AngelHttp(serverApp);
serverApp.use('/api/todos', new s.MapService(autoIdAndDateFields: false));
server = await http.startServer();
var uri = 'http://${server.address.address}:${server.port}';
app = new c.Rest(uri);
list = new c.ServiceList(app.service('api/todos'));
queue = new StreamQueue(list.onChange);
});
tearDown(() async {
await server.close(force: true);
unawaited(list.close());
unawaited(list.service.close());
unawaited(app.close());
});
test('listens on create', () async {
unawaited(list.service.create({'foo': 'bar'}));
await list.onChange.first;
expect(list, [
{'foo': 'bar'}
]);
});
test('listens on modify', () async {
unawaited(list.service.create({'id': 1, 'foo': 'bar'}));
await queue.next;
await list.service.update(1, {'id': 1, 'bar': 'baz'});
await queue.next;
expect(list, [
{'id': 1, 'bar': 'baz'}
]);
});
test('listens on remove', () async {
unawaited(list.service.create({'id': '1', 'foo': 'bar'}));
await queue.next;
await list.service.remove('1');
await queue.next;
expect(list, isEmpty);
});
}

View file

@ -0,0 +1,28 @@
import 'package:angel_model/angel_model.dart';
class Postcard extends Model {
String location;
String message;
Postcard({String id, this.location, this.message}) {
this.id = id;
}
factory Postcard.fromJson(Map data) => new Postcard(
id: data['id'].toString(),
location: data['location'].toString(),
message: data['message'].toString());
@override
bool operator ==(other) {
if (!(other is Postcard)) return false;
return id == other.id &&
location == other.location &&
message == other.message;
}
Map toJson() {
return {'id': id, 'location': location, 'message': message};
}
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Client</title>
</head>
<body>
<script src="main.dart.js"></script>
</body>
</html>

View file

@ -0,0 +1,8 @@
import 'dart:html';
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.baseUrl.toString());
}