Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyv - adding in store property #1188

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/keyv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,21 @@ keyv.ttl = undefined;
console.log(keyv.ttl); // undefined (never expires)
```

## .store

Type: `Storage adapter instance`<br />
Default: `new Map()`

The storage adapter instance to be used by Keyv. This will wire up the iterator, events, and more when a set happens. If it is not a valid Map or Storage Adapter it will throw an error.

```js
import KeyvSqlite from '@keyv/sqlite';
const keyv = new Keyv();
console.log(keyv.store instanceof Map); // true
keyv.store = new KeyvSqlite('sqlite://path/to/database.sqlite');
console.log(keyv.store instanceof KeyvSqlite); // true
```

# How to Contribute

We welcome contributions to Keyv! 🎉 Here are some guides to get you started with contributing:
Expand Down
64 changes: 51 additions & 13 deletions packages/keyv/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export class Keyv<GenericValue = any> extends EventManager {
*/
private _namespace?: string;

/**
* Store
*/
private _store: KeyvStoreAdapter | Map<any, any> | any = new Map();

/**
* Keyv Constructor
* @param {KeyvStoreAdapter | KeyvOptions | Map<any, any>} store to be provided or just the options
Expand Down Expand Up @@ -140,6 +145,8 @@ export class Keyv<GenericValue = any> extends EventManager {
};
}

this._store = this.opts.store;

if (this.opts.compression) {
const compression = this.opts.compression;
this.opts.serialize = compression.serialize.bind(compression);
Expand All @@ -150,23 +157,23 @@ export class Keyv<GenericValue = any> extends EventManager {
this._namespace = this.opts.namespace;
}

if (this.opts.store) {
if (!this._isValidStorageAdapter(this.opts.store)) {
if (this._store) {
if (!this._isValidStorageAdapter(this._store)) {
throw new Error('Invalid storage adapter');
}

if (typeof this.opts.store.on === 'function' && this.opts.emitErrors) {
this.opts.store.on('error', (error: any) => this.emit('error', error));
if (typeof this._store.on === 'function' && this.opts.emitErrors) {
this._store.on('error', (error: any) => this.emit('error', error));
}

this.opts.store.namespace = this._namespace;
this._store.namespace = this._namespace;

// Attach iterators
// @ts-ignore
if (typeof this.opts.store[Symbol.iterator] === 'function' && this.opts.store instanceof Map) {
this.iterator = this.generateIterator((this.opts.store as unknown as IteratorFunction));
} else if ('iterator' in this.opts.store && this.opts.store.opts && this._checkIterableAdapter()) {
this.iterator = this.generateIterator(this.opts.store.iterator!.bind(this.opts.store));
if (typeof this._store[Symbol.iterator] === 'function' && this._store instanceof Map) {
this.iterator = this.generateIterator((this._store as unknown as IteratorFunction));
} else if ('iterator' in this._store && this._store.opts && this._checkIterableAdapter()) {
this.iterator = this.generateIterator(this._store.iterator!.bind(this._store));
}
}

Expand All @@ -179,6 +186,36 @@ export class Keyv<GenericValue = any> extends EventManager {
}
}

/**
* Get the current store
*/
public get store(): KeyvStoreAdapter | Map<any, any> | any {
return this._store;
}

public set store(store: KeyvStoreAdapter | Map<any, any> | any) {
if (this._isValidStorageAdapter(store)) {
this._store = store;
this.opts.store = store;

if (typeof store.on === 'function' && this.opts.emitErrors) {
store.on('error', (error: any) => this.emit('error', error));
}

if (this._namespace) {
this._store.namespace = this._namespace;
}

if (typeof store[Symbol.iterator] === 'function' && store instanceof Map) {
this.iterator = this.generateIterator((store as unknown as IteratorFunction));
} else if ('iterator' in store && store.opts && this._checkIterableAdapter()) {
this.iterator = this.generateIterator(store.iterator!.bind(store));
}
} else {
throw new Error('Invalid storage adapter');
}
}

/**
* Get the current namespace.
* @returns {string | undefined} The current namespace.
Expand All @@ -194,6 +231,7 @@ export class Keyv<GenericValue = any> extends EventManager {
public set namespace(namespace: string | undefined) {
this._namespace = namespace;
this.opts.namespace = namespace;
this._store.namespace = namespace;
if (this.opts.store) {
this.opts.store.namespace = namespace;
}
Expand All @@ -219,10 +257,10 @@ export class Keyv<GenericValue = any> extends EventManager {
generateIterator(iterator: IteratorFunction): IteratorFunction {
const function_: IteratorFunction = async function * (this: any) {
for await (const [key, raw] of (typeof iterator === 'function'
? iterator(this.opts.store.namespace)
? iterator(this._store.namespace)
: iterator)) {
const data = await this.opts.deserialize(raw);
if (this.opts.store.namespace && !key.includes(this.opts.store.namespace)) {
if (this._store.namespace && !key.includes(this._store.namespace)) {
continue;
}

Expand All @@ -239,8 +277,8 @@ export class Keyv<GenericValue = any> extends EventManager {
}

_checkIterableAdapter(): boolean {
return iterableAdapters.includes((this.opts.store.opts.dialect as string))
|| iterableAdapters.some(element => (this.opts.store.opts.url as string).includes(element));
return iterableAdapters.includes((this._store.opts.dialect as string))
|| iterableAdapters.some(element => (this._store.opts.url as string).includes(element));
}

_getKeyPrefix(key: string): string {
Expand Down
36 changes: 36 additions & 0 deletions packages/keyv/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,35 @@ test.it('Keyv accepts storage adapters instead of options', async t => {
t.expect(store.size).toBe(1);
});

test.it('Keyv allows get and set the store via property', async t => {
const store = new Map();
const keyv = new Keyv<string>();
keyv.store = store;
t.expect(store.size).toBe(0);
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
t.expect(await keyv.get('foo', {raw: true})).toEqual({value: 'bar', expires: null});
t.expect(store.size).toBe(1);
t.expect(keyv.store).toBe(store);
});

test.it('Keyv should throw if invalid storage or Map on store property', async t => {
const store = new Map();
const keyv = new Keyv<string>();
keyv.store = store;
t.expect(store.size).toBe(0);
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
t.expect(await keyv.get('foo', {raw: true})).toEqual({value: 'bar', expires: null});
t.expect(store.size).toBe(1);
t.expect(keyv.store).toBe(store);

t.expect(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
keyv.store = {get() {}, set() {}, delete() {}};
}).toThrow();
});

test.it('Keyv passes ttl info to stores', async t => {
t.expect.assertions(1);
const store = new Map();
Expand Down Expand Up @@ -660,6 +689,13 @@ test.it('should be able to set the namespace via property', async t => {
t.expect(store.namespace).toBe('test');
});

test.it('should be able to set the store via property', async t => {
const store = new KeyvSqlite({uri: 'sqlite://test/testdb.sqlite'});
const keyv = new Keyv();
keyv.store = store;
t.expect(keyv.store).toBe(store);
});

test.it('Keyv respects default ttl option', async t => {
const store = new Map();
const keyv = new Keyv({store, ttl: 100});
Expand Down
Loading