Add 'packages/client/' from commit '180edbc46a556f6d572c3b4ade4b396a31a1bc42'
git-subtree-dir: packages/client git-subtree-mainline:ae0afd3408
git-subtree-split:180edbc46a
This commit is contained in:
commit
998aa62303
27 changed files with 1597 additions and 0 deletions
4
packages/client/.babelrc
Normal file
4
packages/client/.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["add-module-exports"]
|
||||
}
|
81
packages/client/.gitignore
vendored
Normal file
81
packages/client/.gitignore
vendored
Normal 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
|
10
packages/client/.npmignore
Normal file
10
packages/client/.npmignore
Normal file
|
@ -0,0 +1,10 @@
|
|||
.babelrc
|
||||
.istanbul.yml
|
||||
.travis.yml
|
||||
.editorconfig
|
||||
.idea/
|
||||
src/
|
||||
test/
|
||||
!lib/
|
||||
.github/
|
||||
coverage
|
4
packages/client/.travis.yml
Normal file
4
packages/client/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- dev
|
||||
- stable
|
43
packages/client/CHANGELOG.md
Normal file
43
packages/client/CHANGELOG.md
Normal 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
21
packages/client/LICENSE
Normal 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
106
packages/client/README.md
Normal 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,
|
||||
);
|
||||
}
|
||||
```
|
6
packages/client/analysis_options.yaml
Normal file
6
packages/client/analysis_options.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
# errors:
|
||||
# unawaited_futures: ignore
|
14
packages/client/build.yaml
Normal file
14
packages/client/build.yaml
Normal 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
|
1
packages/client/dart_test.yaml
Normal file
1
packages/client/dart_test.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
# platforms: [vm, content-shell]
|
21
packages/client/example/main.dart
Normal file
21
packages/client/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};
|
||||
}
|
347
packages/client/lib/angel_client.dart
Normal file
347
packages/client/lib/angel_client.dart
Normal 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();
|
||||
}
|
||||
}
|
4
packages/client/lib/auth_types.dart
Normal file
4
packages/client/lib/auth_types.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
@deprecated
|
||||
library auth_types;
|
||||
|
||||
const String local = 'local', token = 'token';
|
439
packages/client/lib/base_angel_client.dart
Normal file
439
packages/client/lib/base_angel_client.dart
Normal 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;
|
||||
}
|
||||
}
|
80
packages/client/lib/browser.dart
Normal file
80
packages/client/lib/browser.dart
Normal 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();
|
||||
}
|
||||
}
|
19
packages/client/lib/flutter.dart
Normal file
19
packages/client/lib/flutter.dart
Normal 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.');
|
||||
}
|
||||
}
|
68
packages/client/lib/io.dart
Normal file
68
packages/client/lib/io.dart
Normal 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);
|
||||
}
|
||||
}
|
35
packages/client/package.json
Normal file
35
packages/client/package.json
Normal 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"
|
||||
}
|
||||
}
|
23
packages/client/pubspec.yaml
Normal file
23
packages/client/pubspec.yaml
Normal 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
BIN
packages/client/test/.DS_Store
vendored
Normal file
Binary file not shown.
85
packages/client/test/all_test.dart
Normal file
85
packages/client/test/all_test.dart
Normal 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'}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
70
packages/client/test/common.dart
Normal file
70
packages/client/test/common.dart
Normal 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();
|
||||
}
|
||||
}
|
10
packages/client/test/index.html
Normal file
10
packages/client/test/index.html
Normal 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>
|
61
packages/client/test/list_test.dart
Normal file
61
packages/client/test/list_test.dart
Normal 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);
|
||||
});
|
||||
}
|
28
packages/client/test/shared.dart
Normal file
28
packages/client/test/shared.dart
Normal 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};
|
||||
}
|
||||
}
|
9
packages/client/web/index.html
Normal file
9
packages/client/web/index.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Client</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="main.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
8
packages/client/web/main.dart
Normal file
8
packages/client/web/main.dart
Normal 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());
|
||||
}
|
Loading…
Reference in a new issue