Skip to content

Commit

Permalink
Merge pull request #12 from keyvhq/update+readme
Browse files Browse the repository at this point in the history
Update readme, remove `keyv` references
  • Loading branch information
Kikobeats committed Jun 19, 2021
2 parents 0f5da85 + 5704cca commit c204f58
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 75 deletions.
48 changes: 24 additions & 24 deletions README.md
Expand Up @@ -39,25 +39,33 @@ npm install --save keyv
By default everything is stored in memory, you can optionally also install a storage adapter.

```
npm install --save @keyv/redis
npm install --save @keyv/mongo
npm install --save @keyv/sqlite
npm install --save @keyv/postgres
npm install --save @keyv/mysql
npm install --save @keyvhq/keyv-redis
npm install --save @keyvhq/keyv-mongo
npm install --save @keyvhq/keyv-sqlite
npm install --save @keyvhq/keyv-postgres
npm install --save @keyvhq/keyv-mysql
```

Create a new Keyv instance, passing your connection string if applicable. Keyv will automatically load the correct storage adapter.

```js
const Keyv = require('keyv');
const Keyv = require('@keyvhq/keyv');
const KeyvRedis = require('@keyvhq/keyv-redis');
const KeyvMongo = require('@keyvhq/keyv-mongo');
const KeyvMySQL = require('@keyvhq/keyv-mysql');
const KeyvSQLite = require('@keyvhq/keyv-sqlite');
const KeyvPostgreSQL = require('@keyvhq/keyv-postgres');


// One of the following
const keyv = new Keyv();
const keyv = new Keyv('redis://user:pass@localhost:6379');
const keyv = new Keyv('mongodb://user:pass@localhost:27017/dbname');
const keyv = new Keyv('sqlite://path/to/database.sqlite');
const keyv = new Keyv('postgresql://user:pass@localhost:5432/dbname');
const keyv = new Keyv('mysql://user:pass@localhost:3306/dbname');

const keyvRedis = new Keyv({ store: new KeyvRedis('redis://user:pass@localhost:6379')})
const keyv = new Keyv();
const keyv = new Keyv({ store: new KeyvMongo('mongodb://user:pass@localhost:27017/dbname')});
const keyv = new Keyv({ store: new KeyvSQLite('sqlite://path/to/database.sqlite')});
const keyv = new Keyv({ store: new KeyvPostgreSQL('postgresql://user:pass@localhost:5432/dbname')});
const keyv = new Keyv({ store: new KeyvMySQL('mysql://user:pass@localhost:3306/dbname')});

// Handle DB connection errors
keyv.on('error', err => console.log('Connection Error', err));
Expand All @@ -74,8 +82,8 @@ await keyv.clear(); // undefined
You can namespace your Keyv instance to avoid key collisions and allow you to clear only a certain namespace while using the same database.

```js
const users = new Keyv('redis://user:pass@localhost:6379', { namespace: 'users' });
const cache = new Keyv('redis://user:pass@localhost:6379', { namespace: 'cache' });
const users = new Keyv({ store: new KeyvRedis('redis://user:pass@localhost:6379'), namespace: 'users' });
const cache = new Keyv({ store: new KeyvRedis('redis://user:pass@localhost:6379'), namespace: 'cache' });

await users.set('foo', 'users'); // true
await cache.set('foo', 'cache'); // true
Expand All @@ -100,22 +108,14 @@ const keyv = new Keyv({ serialize: JSON.stringify, deserialize: JSON.parse });

## Official Storage Adapters

The official storage adapters are covered by [over 150 integration tests](https://travis-ci.org/lukechilds/keyv/jobs/260418145) to guarantee consistent behaviour. They are lightweight, efficient wrappers over the DB clients making use of indexes and native TTLs where available.

Database | Adapter | Native TTL | Status
---|---|---|---
Redis | [@keyv/redis](https://github.com/lukechilds/keyv-redis) | Yes | [![Build Status](https://travis-ci.org/lukechilds/keyv-redis.svg?branch=master)](https://travis-ci.org/lukechilds/keyv-redis) [![Coverage Status](https://coveralls.io/repos/github/lukechilds/keyv-redis/badge.svg?branch=master)](https://coveralls.io/github/lukechilds/keyv-redis?branch=master)
MongoDB | [@keyv/mongo](https://github.com/lukechilds/keyv-mongo) | Yes | [![Build Status](https://travis-ci.org/lukechilds/keyv-mongo.svg?branch=master)](https://travis-ci.org/lukechilds/keyv-mongo) [![Coverage Status](https://coveralls.io/repos/github/lukechilds/keyv-mongo/badge.svg?branch=master)](https://coveralls.io/github/lukechilds/keyv-mongo?branch=master)
SQLite | [@keyv/sqlite](https://github.com/lukechilds/keyv-sqlite) | No | [![Build Status](https://travis-ci.org/lukechilds/keyv-sqlite.svg?branch=master)](https://travis-ci.org/lukechilds/keyv-sqlite) [![Coverage Status](https://coveralls.io/repos/github/lukechilds/keyv-sqlite/badge.svg?branch=master)](https://coveralls.io/github/lukechilds/keyv-sqlite?branch=master)
PostgreSQL | [@keyv/postgres](https://github.com/lukechilds/keyv-postgres) | No | [![Build Status](https://travis-ci.org/lukechilds/keyv-postgres.svg?branch=master)](https://travis-ci.org/lukechildskeyv-postgreskeyv) [![Coverage Status](https://coveralls.io/repos/github/lukechilds/keyv-postgres/badge.svg?branch=master)](https://coveralls.io/github/lukechilds/keyv-postgres?branch=master)
MySQL | [@keyv/mysql](https://github.com/lukechilds/keyv-mysql) | No | [![Build Status](https://travis-ci.org/lukechilds/keyv-mysql.svg?branch=master)](https://travis-ci.org/lukechilds/keyv-mysql) [![Coverage Status](https://coveralls.io/repos/github/lukechilds/keyv-mysql/badge.svg?branch=master)](https://coveralls.io/github/lukechilds/keyv-mysql?branch=master)
The official storage adapters are covered by [over 150 integration tests](https://github.com/keyvhq/keyv/actions/runs/949262324) to guarantee consistent behaviour. They are lightweight, efficient wrappers over the DB clients making use of indexes and native TTLs where available.

## Third-party Storage Adapters

You can also use third-party storage adapters or build your own. Keyv will wrap these storage adapters in TTL functionality and handle complex types internally.

```js
const Keyv = require('keyv');
const Keyv = require('@keyvhq/keyv');
const myAdapter = require('./my-storage-adapter');

const keyv = new Keyv({ store: myAdapter });
Expand All @@ -130,7 +130,7 @@ new Keyv({ store: new Map() });
For example, [`quick-lru`](https://github.com/sindresorhus/quick-lru) is a completely unrelated module that implements the Map API.

```js
const Keyv = require('keyv');
const Keyv = require('@keyvhq/keyv');
const QuickLRU = require('quick-lru');

const lru = new QuickLRU({ maxSize: 1000 });
Expand Down
4 changes: 2 additions & 2 deletions packages/keyv-mongo/src/index.js
Expand Up @@ -96,13 +96,13 @@ class KeyvMongo extends EventEmitter {

clear() {
return this.connect
.then(store => store.deleteMany({ key: new RegExp(`^${this.namespace}:`) })
.then(store => store.deleteMany({ key: new RegExp(`^${this.namespace + ':'}`) })
.then(() => undefined));
}

async * iterator() {
const iterator = await this.connect
.then(store => store.find({ key: new RegExp(`^${this.namespace}:`) }).map(x => {
.then(store => store.find({ key: new RegExp(`^${this.namespace ? this.namespace + ':' : '.*'}`) }).map(x => {
return [x.key, x.value];
}));
yield * iterator;
Expand Down
3 changes: 2 additions & 1 deletion packages/keyv-redis/package.json
Expand Up @@ -34,7 +34,8 @@
},
"homepage": "https://github.com/keyvhq/keyv",
"dependencies": {
"ioredis": "~4.17.1"
"ioredis": "~4.17.1",
"p-event": "~4.2.0"
},
"gitHead": "a4e2c1f285236a753de13eb8a42d9a98690526cc"
}
66 changes: 28 additions & 38 deletions packages/keyv-redis/src/index.js
@@ -1,8 +1,8 @@
'use strict';

const EventEmitter = require('events');
const pEvent = require('p-event');
const Redis = require('ioredis');
const pify = require('pify');

class KeyvRedis extends EventEmitter {
constructor(uri, options) {
Expand All @@ -11,60 +11,50 @@ class KeyvRedis extends EventEmitter {
if (uri instanceof Redis) {
this.redis = uri;
} else {
options = Object.assign({}, typeof uri === 'string' ? { uri } : uri, options);
options = Object.assign(
{},
typeof uri === 'string' ? { uri } : uri,
options
);
this.redis = new Redis(options.uri, options);
}

this.redis.on('error', error => this.emit('error', error));
}

_getNamespace() {
return `namespace:${this.namespace}`;
async get(key) {
const value = await this.redis.get(key);
return value === null ? undefined : value;
}

get(key) {
return this.redis.get(key)
.then(value => {
if (value === null) {
return undefined;
}

return value;
});
}

set(key, value, ttl) {
async set(key, value, ttl) {
if (typeof value === 'undefined') {
return Promise.resolve(undefined);
return undefined;
}

return Promise.resolve()
.then(() => {
if (typeof ttl === 'number') {
return this.redis.set(key, value, 'PX', ttl);
}

return this.redis.set(key, value);
})
.then(() => this.redis.sadd(this._getNamespace(), key));
return typeof ttl === 'number' ?
this.redis.set(key, value, 'PX', ttl) :
this.redis.set(key, value);
}

delete(key) {
return this.redis.del(key)
.then(items => {
return this.redis.srem(this._getNamespace(), key)
.then(() => items > 0);
});
async delete(key) {
const result = await this.redis.unlink(key);
return result > 0;
}

clear() {
return this.redis.smembers(this._getNamespace())
.then(keys => this.redis.del(keys.concat(this._getNamespace())))
.then(() => undefined);
async clear() {
const stream = this.redis.scanStream({ match: `${this.namespace}:*` });

const keys = [];
stream.on('data', matchedKeys => keys.push(...matchedKeys));
await pEvent(stream, 'end');
if (keys.length > 0) {
await this.redis.unlink(keys);
}
}

async * iterator() {
const scan = pify(this.redis.scan).bind(this.redis);
const scan = this.redis.scan.bind(this.redis);
const get = this.redis.mget.bind(this.redis);
async function * iterate(curs, pattern) {
const [cursor, keys] = await scan(curs, 'MATCH', pattern);
Expand All @@ -82,7 +72,7 @@ class KeyvRedis extends EventEmitter {
}
}

yield * iterate(0, `${this.namespace}:*`);
yield * iterate(0, `${this.namespace ? this.namespace + ':' : ''}*`);
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/keyv-sql/src/index.js
Expand Up @@ -65,14 +65,14 @@ class KeyvSql extends EventEmitter {
}

clear() {
const del = this.options.dialect === 'mysql' ? `DELETE FROM \`${this.options.table}\` WHERE (\`${this.options.table}\`.\`key\` LIKE '${this.namespace}:%')` : `DELETE FROM "${this.options.table}" WHERE ("${this.options.table}"."key" LIKE '${this.namespace}:%')`;
const del = this.options.dialect === 'mysql' ? `DELETE FROM \`${this.options.table}\` WHERE (\`${this.options.table}\`.\`key\` LIKE '${this.namespace ? this.namespace + ':' : ''}%')` : `DELETE FROM "${this.options.table}" WHERE ("${this.options.table}"."key" LIKE '${this.namespace ? this.namespace + ':' : ''}%')`;
return this.query(del)
.then(() => undefined);
}

async * iterator() {
const limit = Number.parseInt(this.options.iterationLimit, 10);
const selectChunk = this.options.dialect === 'mysql' ? `SELECT * FROM \`${this.options.table}\` WHERE (\`${this.options.table}\`.\`key\` LIKE '${this.namespace}:%') LIMIT ${limit} OFFSET ` : `SELECT * FROM "${this.options.table}" WHERE ("${this.options.table}"."key" LIKE '${this.namespace}:%') LIMIT ${limit} OFFSET `;
const selectChunk = this.options.dialect === 'mysql' ? `SELECT * FROM \`${this.options.table}\` WHERE (\`${this.options.table}\`.\`key\` LIKE '${this.namespace ? this.namespace + ':' : ''}%') LIMIT ${limit} OFFSET ` : `SELECT * FROM "${this.options.table}" WHERE ("${this.options.table}"."key" LIKE '${this.namespace ? this.namespace + ':' : ''}%') LIMIT ${limit} OFFSET `;

async function * iterate(offset, query) {
const entries = await query(selectChunk + offset);
Expand Down
1 change: 1 addition & 0 deletions packages/keyv-test-suite/src/api.js
Expand Up @@ -67,6 +67,7 @@ const keyvApiTests = (test, Keyv, store) => {

test.serial('.delete(key) with nonexistent key resolves to false', async t => {
const keyv = new Keyv({ store: store() });
t.is(await keyv.delete(), false);
t.is(await keyv.delete('foo'), false);
});

Expand Down
6 changes: 3 additions & 3 deletions packages/keyv-test-suite/src/index.js
@@ -1,19 +1,19 @@
const keyvApiTests = require('./api.js');
const keyvValueTests = require('./values.js');
const keyvNamepsaceTests = require('./namespace.js');
const keyvNamespaceTests = require('./namespace.js');
const keyvIteratorTests = require('./iteration.js');

const keyvTestSuite = (test, Keyv, store) => {
keyvIteratorTests(test, Keyv, store);
keyvApiTests(test, Keyv, store);
keyvValueTests(test, Keyv, store);
keyvNamepsaceTests(test, Keyv, store);
keyvNamespaceTests(test, Keyv, store);
};

Object.assign(keyvTestSuite, {
keyvApiTests,
keyvValueTests,
keyvNamepsaceTests,
keyvNamespaceTests,
keyvIteratorTests
});

Expand Down
14 changes: 14 additions & 0 deletions packages/keyv-test-suite/src/namespace.js
Expand Up @@ -39,6 +39,20 @@ const keyvNamepsaceTests = (test, Keyv, store) => {
t.is(await keyv2.get('bar'), 'keyv2');
});

test.serial('no namespaced clear doesn\'t clears all stores', async t => {
const keyv1 = new Keyv({ store: store(), namespace: false });
const keyv2 = new Keyv({ store: store(), namespace: 'keyv2' });
await keyv1.set('foo', 'keyv1');
await keyv1.set('bar', 'keyv1');
await keyv2.set('foo', 'keyv2');
await keyv2.set('bar', 'keyv2');
await keyv1.clear();
t.is(await keyv1.get('foo'), 'keyv1');
t.is(await keyv1.get('bar'), 'keyv1');
t.is(await keyv2.get('foo'), 'keyv2');
t.is(await keyv2.get('bar'), 'keyv2');
});

test.after.always(async () => {
const keyv1 = new Keyv({ store: store(), namespace: 'keyv1' });
const keyv2 = new Keyv({ store: store(), namespace: 'keyv2' });
Expand Down
2 changes: 1 addition & 1 deletion packages/keyv/package.json
@@ -1,6 +1,6 @@
{
"name": "@keyvhq/keyv",
"version": "0.2.2",
"version": "0.2.3",
"description": "Simple key-value storage with support for multiple backends",
"main": "src/index.js",
"scripts": {
Expand Down
12 changes: 8 additions & 4 deletions packages/keyv/src/index.js
Expand Up @@ -21,12 +21,12 @@ class Keyv extends EventEmitter {
this.store = new Map();
}

this.store.namespace = this.options.namespace;

if (typeof this.store.on === 'function') {
this.store.on('error', error => this.emit('error', error));
}

this.store.namespace = this.options.namespace;

const generateIterator = iterator => async function * () {
for await (const [key, raw] of (typeof iterator === 'function' ? iterator() : iterator)) {
const data = (typeof raw === 'string') ? this.options.deserialize(raw) : raw;
Expand Down Expand Up @@ -54,11 +54,11 @@ class Keyv extends EventEmitter {
}

_getKeyPrefix(key) {
return `${this.options.namespace}:${key}`;
return this.options.namespace ? `${this.options.namespace}:${key}` : key;
}

_getKeyUnprefix(key) {
return key.split(':').splice(1).join(':');
return this.options.namespace ? key.split(':').splice(1).join(':') : key;
}

get(key, options) {
Expand Down Expand Up @@ -112,6 +112,10 @@ class Keyv extends EventEmitter {
}

clear() {
if (!this.options.namespace) {
return Promise.resolve().then(() => undefined);
}

const store = this.store;
return Promise.resolve()
.then(() => store.clear());
Expand Down
21 changes: 21 additions & 0 deletions packages/keyv/test/keyv.js
Expand Up @@ -124,5 +124,26 @@ test.serial('Keyv supports async serializer/deserializer', async t => {
t.is(await keyv.get('foo'), 'bar');
});

test.serial('Keyv uses a default namespace', async t => {
const store = new Map();
const keyv = new Keyv({ store });
await keyv.set('foo', 'bar');
t.is([...store.keys()][0], 'keyv:foo');
});

test.serial('Default namespace can be overridden', async t => {
const store = new Map();
const keyv = new Keyv({ store, namespace: 'magic' });
await keyv.set('foo', 'bar');
t.is([...store.keys()][0], 'magic:foo');
});

test.serial('An empty namespace stores the key as-is', async t => {
const store = new Map();
const keyv = new Keyv({ store, namespace: '' });
await keyv.set(42, 'foo');
t.is([...store.keys()][0], 42);
});

const store = () => new Map();
keyvTestSuite(test, Keyv, store);

0 comments on commit c204f58

Please sign in to comment.