Add 'packages/redis/' from commit '929568c8660ecb4e6ddf1b6b616ab5591cb95419'

git-subtree-dir: packages/redis
git-subtree-mainline: 26afe42cb7
git-subtree-split: 929568c866
This commit is contained in:
Tobe O 2020-02-15 18:29:13 -05:00
commit ae8f7a77d2
10 changed files with 317 additions and 0 deletions

14
packages/redis/.gitignore vendored Normal file
View 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

View file

@ -0,0 +1,3 @@
language: dart
services:
- redis-server

View file

@ -0,0 +1,2 @@
# 1.0.0
* First version.

21
packages/redis/LICENSE Normal file
View 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
View 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();
}
```

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

View file

@ -0,0 +1 @@
export 'src/redis_service.dart';

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

View 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

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