Add 'packages/redis/' from commit '929568c8660ecb4e6ddf1b6b616ab5591cb95419'
git-subtree-dir: packages/redis git-subtree-mainline:26afe42cb7
git-subtree-split:929568c866
This commit is contained in:
commit
ae8f7a77d2
10 changed files with 317 additions and 0 deletions
14
packages/redis/.gitignore
vendored
Normal file
14
packages/redis/.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
.dart_tool
|
||||
dump.rdb
|
3
packages/redis/.travis.yml
Normal file
3
packages/redis/.travis.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
language: dart
|
||||
services:
|
||||
- redis-server
|
2
packages/redis/CHANGELOG.md
Normal file
2
packages/redis/CHANGELOG.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# 1.0.0
|
||||
* First version.
|
21
packages/redis/LICENSE
Normal file
21
packages/redis/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 The Angel Framework
|
||||
|
||||
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.
|
66
packages/redis/README.md
Normal file
66
packages/redis/README.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# redis
|
||||
[![Pub](https://img.shields.io/pub/v/angel_redis.svg)](https://pub.dartlang.org/packages/angel_redis)
|
||||
[![build status](https://travis-ci.org/angel-dart/redis.svg)](https://travis-ci.org/angel-dart/redis)
|
||||
|
||||
Redis-enabled services for the Angel framework.
|
||||
`RedisService` can be used alone, *or* as the backend of a
|
||||
[`CacheService`](https://github.com/angel-dart/cache),
|
||||
and thereby cache the results of calling an upstream database.
|
||||
|
||||
## Installation
|
||||
`package:angel_redis` requires Angel 2.
|
||||
|
||||
In your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel_framework: ^2.0.0-alpha
|
||||
angel_redis: ^1.0.0
|
||||
```
|
||||
|
||||
## Usage
|
||||
Pass an instance of `RespCommands` (from `package:resp_client`) to the `RedisService` constructor.
|
||||
You can also pass an optional prefix, which is recommended if you are using `angel_redis` for multiple
|
||||
logically-separate collections. Redis is a flat key-value store; by prefixing the keys used,
|
||||
`angel_redis` can provide the experience of using separate stores, rather than a single node.
|
||||
|
||||
Without a prefix, there's a chance that different collections can overwrite one another's data.
|
||||
|
||||
## Notes
|
||||
* Neither `index`, nor `modify` is atomic; each performs two separate queries.`angel_redis` stores data as JSON strings, rather than as Redis hashes, so an update-in-place is impossible.
|
||||
* `index` uses Redis' `KEYS` functionality, so use it sparingly in production, if at all. In a larger database, it can quickly
|
||||
become a bottleneck.
|
||||
* `remove` uses `MULTI`+`EXEC` in a transaction.
|
||||
* Prefer using `update`, rather than `modify`. The former only performs one query, though it does overwrite the current
|
||||
contents for a given key.
|
||||
* When calling `create`, it's possible that you may already have an `id` in mind to insert into the store. For example,
|
||||
when caching another database, you'll preserve the ID or primary key of an item. `angel_redis` heeds this. If no
|
||||
`id` is present, then an ID will be created via an `INCR` call.
|
||||
|
||||
## Example
|
||||
Also present at `example/main.dart`:
|
||||
|
||||
```dart
|
||||
import 'package:angel_redis/angel_redis.dart';
|
||||
import 'package:resp_client/resp_client.dart';
|
||||
import 'package:resp_client/resp_commands.dart';
|
||||
|
||||
main() async {
|
||||
var connection = await connectSocket('localhost');
|
||||
var client = new RespClient(connection);
|
||||
var service = new RedisService(new RespCommands(client), prefix: 'example');
|
||||
|
||||
// Create an object
|
||||
await service.create({'id': 'a', 'hello': 'world'});
|
||||
|
||||
// Read it...
|
||||
var read = await service.read('a');
|
||||
print(read['hello']);
|
||||
|
||||
// Delete it.
|
||||
await service.remove('a');
|
||||
|
||||
// Close the connection.
|
||||
await connection.close();
|
||||
}
|
||||
```
|
22
packages/redis/example/main.dart
Normal file
22
packages/redis/example/main.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:angel_redis/angel_redis.dart';
|
||||
import 'package:resp_client/resp_client.dart';
|
||||
import 'package:resp_client/resp_commands.dart';
|
||||
|
||||
main() async {
|
||||
var connection = await connectSocket('localhost');
|
||||
var client = new RespClient(connection);
|
||||
var service = new RedisService(new RespCommands(client), prefix: 'example');
|
||||
|
||||
// Create an object
|
||||
await service.create({'id': 'a', 'hello': 'world'});
|
||||
|
||||
// Read it...
|
||||
var read = await service.read('a');
|
||||
print(read['hello']);
|
||||
|
||||
// Delete it.
|
||||
await service.remove('a');
|
||||
|
||||
// Close the connection.
|
||||
await connection.close();
|
||||
}
|
1
packages/redis/lib/angel_redis.dart
Normal file
1
packages/redis/lib/angel_redis.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/redis_service.dart';
|
97
packages/redis/lib/src/redis_service.dart
Normal file
97
packages/redis/lib/src/redis_service.dart
Normal file
|
@ -0,0 +1,97 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:resp_client/resp_client.dart';
|
||||
import 'package:resp_client/resp_commands.dart';
|
||||
|
||||
/// An Angel service that reads and writes JSON within a Redis store.
|
||||
class RedisService extends Service<String, Map<String, dynamic>> {
|
||||
final RespCommands respCommands;
|
||||
|
||||
/// An optional string prefixed to keys before they are inserted into Redis.
|
||||
///
|
||||
/// Consider using this if you are using several different Redis collections
|
||||
/// within a single application.
|
||||
final String prefix;
|
||||
|
||||
RedisService(this.respCommands, {this.prefix});
|
||||
|
||||
String _applyPrefix(String id) => prefix == null ? id : '$prefix:$id';
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> index(
|
||||
[Map<String, dynamic> params]) async {
|
||||
var result =
|
||||
await respCommands.client.writeArrayOfBulk(['KEYS', _applyPrefix('*')]);
|
||||
var keys = result.payload.map((RespType s) => s.payload) as Iterable;
|
||||
if (keys.isEmpty) return [];
|
||||
result = await respCommands.client.writeArrayOfBulk(['MGET']..addAll(keys));
|
||||
return result.payload
|
||||
.map<Map<String, dynamic>>(
|
||||
(RespType s) => json.decode(s.payload) as Map<String, dynamic>)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> read(String id,
|
||||
[Map<String, dynamic> params]) async {
|
||||
var value = await respCommands.get(_applyPrefix(id));
|
||||
|
||||
if (value == null) {
|
||||
throw new AngelHttpException.notFound(
|
||||
message: 'No record found for ID $id');
|
||||
} else {
|
||||
return json.decode(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> create(Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
String id;
|
||||
if (data['id'] != null)
|
||||
id = data['id'] as String;
|
||||
else {
|
||||
var keyVar = await respCommands.client
|
||||
.writeArrayOfBulk(['INCR', _applyPrefix('angel_redis:id')]);
|
||||
id = keyVar.payload.toString();
|
||||
data = new Map<String, dynamic>.from(data)..['id'] = id;
|
||||
}
|
||||
|
||||
await respCommands.set(_applyPrefix(id), json.encode(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> modify(String id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
var input = await read(id);
|
||||
input.addAll(data);
|
||||
return await update(id, input, params);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> update(String id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic> params]) async {
|
||||
data = new Map<String, dynamic>.from(data)..['id'] = id;
|
||||
await respCommands.set(_applyPrefix(id), json.encode(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> remove(String id,
|
||||
[Map<String, dynamic> params]) async {
|
||||
var client = respCommands.client;
|
||||
await client.writeArrayOfBulk(['MULTI']);
|
||||
await client.writeArrayOfBulk(['GET', _applyPrefix(id)]);
|
||||
await client.writeArrayOfBulk(['DEL', _applyPrefix(id)]);
|
||||
var result = await client.writeArrayOfBulk(['EXEC']);
|
||||
var str = result.payload[0] as RespBulkString;
|
||||
|
||||
if (str.payload == null)
|
||||
throw new AngelHttpException.notFound(
|
||||
message: 'No record found for ID $id');
|
||||
else
|
||||
return json.decode(str.payload);
|
||||
}
|
||||
}
|
13
packages/redis/pubspec.yaml
Normal file
13
packages/redis/pubspec.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: angel_redis
|
||||
version: 1.0.0
|
||||
description: An Angel service provider for Redis. Works well for caching volatile data.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/redis
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
dependencies:
|
||||
angel_framework: ^2.0.0-alpha
|
||||
angel_http_exception: ^1.0.0
|
||||
resp_client: ^0.1.6
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
78
packages/redis/test/all_test.dart
Normal file
78
packages/redis/test/all_test.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:angel_redis/angel_redis.dart';
|
||||
import 'package:resp_client/resp_client.dart';
|
||||
import 'package:resp_client/resp_commands.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() async {
|
||||
RespServerConnection connection;
|
||||
RedisService service;
|
||||
|
||||
setUp(() async {
|
||||
connection = await connectSocket('localhost');
|
||||
service = new RedisService(new RespCommands(new RespClient(connection)),
|
||||
prefix: 'angel_redis_test');
|
||||
});
|
||||
|
||||
tearDown(() => connection.close());
|
||||
|
||||
test('index', () async {
|
||||
// Wipe
|
||||
await service.respCommands.flushDb();
|
||||
await service.create({'id': '0', 'name': 'Tobe'});
|
||||
await service.create({'id': '1', 'name': 'Osakwe'});
|
||||
|
||||
var output = await service.index();
|
||||
expect(output, hasLength(2));
|
||||
expect(output[1], <String, dynamic>{'id': '0', 'name': 'Tobe'});
|
||||
expect(output[0], <String, dynamic>{'id': '1', 'name': 'Osakwe'});
|
||||
});
|
||||
|
||||
test('create with id', () async {
|
||||
var input = {'id': 'foo', 'bar': 'baz'};
|
||||
var output = await service.create(input);
|
||||
expect(input, output);
|
||||
});
|
||||
|
||||
test('create without id', () async {
|
||||
var input = {'bar': 'baz'};
|
||||
var output = await service.create(input);
|
||||
print(output);
|
||||
expect(output.keys, contains('id'));
|
||||
expect(output, containsPair('bar', 'baz'));
|
||||
});
|
||||
|
||||
test('read', () async {
|
||||
var id = 'poobah${new DateTime.now().millisecondsSinceEpoch}';
|
||||
var input = await service.create({'id': id, 'bar': 'baz'});
|
||||
expect(await service.read(id), input);
|
||||
});
|
||||
|
||||
test('modify', () async {
|
||||
var id = 'jamboree${new DateTime.now().millisecondsSinceEpoch}';
|
||||
await service.create({'id': id, 'bar': 'baz', 'yes': 'no'});
|
||||
var output = await service.modify(id, {'bar': 'quux'});
|
||||
expect(output, {'id': id, 'bar': 'quux', 'yes': 'no'});
|
||||
expect(await service.read(id), output);
|
||||
});
|
||||
|
||||
test('update', () async {
|
||||
var id = 'hoopla${new DateTime.now().millisecondsSinceEpoch}';
|
||||
await service.create({'id': id, 'bar': 'baz'});
|
||||
var output = await service.update(id, {'yes': 'no'});
|
||||
expect(output, {'id': id, 'yes': 'no'});
|
||||
expect(await service.read(id), output);
|
||||
});
|
||||
|
||||
test('remove', () async {
|
||||
var id = 'gelatin${new DateTime.now().millisecondsSinceEpoch}';
|
||||
var input = await service.create({'id': id, 'bar': 'baz'});
|
||||
expect(await service.remove(id), input);
|
||||
expect(await service.respCommands.exists([id]), 0);
|
||||
});
|
||||
|
||||
test('remove nonexistent', () async {
|
||||
expect(() => service.remove('definitely_does_not_exist'),
|
||||
throwsA(const TypeMatcher<AngelHttpException>()));
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue