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
|
||||||
|
[](https://pub.dartlang.org/packages/angel_redis)
|
||||||
|
[](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