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

Add validation method for storage adapters #1110

Merged
merged 4 commits into from
Aug 17, 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
23 changes: 18 additions & 5 deletions packages/keyv/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ export type KeyvOptions = {
/** A custom deserialization function. */
deserialize?: CompressionAdapter['deserialize'];
/** The storage adapter instance to be used by Keyv. */
store?: KeyvStoreAdapter | Map<any, any>;
store?: KeyvStoreAdapter | Map<any, any> | any;
/** Default TTL. Can be overridden by specifying a TTL on `.set()`. */
ttl?: number;
/** Enable compression option **/
compression?: CompressionAdapter;
compression?: CompressionAdapter | any;
/** Enable or disable statistics (default is false) */
stats?: boolean;
};
Expand All @@ -92,7 +92,6 @@ class Keyv extends EventManager {

constructor(store?: KeyvStoreAdapter | KeyvOptions | Map<any, any>, options?: Omit<KeyvOptions, 'store'>);
constructor(options?: KeyvOptions);

constructor(store?: KeyvStoreAdapter | KeyvOptions, options?: Omit<KeyvOptions, 'store'>) {
super();
options ??= {};
Expand All @@ -111,7 +110,6 @@ class Keyv extends EventManager {
if (store && (store as KeyvStoreAdapter).get) {
this.opts.store = store as KeyvStoreAdapter;
} else {
// @ts-expect-error - Map is not a KeyvStoreAdapter
this.opts = {
...this.opts,
...store,
Expand All @@ -125,6 +123,10 @@ class Keyv extends EventManager {
}

if (this.opts.store) {
if (!this._isValidStorageAdapter(this.opts.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));
}
Expand Down Expand Up @@ -169,7 +171,7 @@ class Keyv extends EventManager {

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

_getKeyPrefix(key: string): string {
Expand All @@ -187,6 +189,17 @@ class Keyv extends EventManager {
.join(':');
}

_isValidStorageAdapter(store: KeyvStoreAdapter | any): boolean {
return (
store instanceof Map || (
typeof store.get === 'function'
&& typeof store.set === 'function'
&& typeof store.delete === 'function'
&& typeof store.clear === 'function'
)
);
}

async get<Value>(key: string, options?: {raw: false}): Promise<StoredDataNoRaw<Value>>;
async get<Value>(key: string, options?: {raw: true}): Promise<StoredDataRaw<Value>>;
async get<Value>(key: string[], options?: {raw: false}): Promise<Array<StoredDataNoRaw<Value>>>;
Expand Down
19 changes: 15 additions & 4 deletions packages/keyv/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ test.it('Keyv should wait for the expired get', async t => {
set(key: string, value: any) {
_store.set(key, value);
},
clear() {
_store.clear();
},
async delete(key: string) {
await new Promise<void>(resolve => {
setTimeout(() => {
Expand Down Expand Up @@ -207,6 +210,18 @@ test.it('Keyv should wait for the expired get', async t => {
t.expect(v4).toBe('bar');
});

test.it('keyv should trigger an error when store is invalid', async t => {
const store = new Map();

t.expect(() => new Keyv({
store: {
async get(key: string) {
store.get(key);
},
},
})).toThrow();
});

test.it('.delete([keys]) should delete multiple keys for storage adapter not supporting deleteMany', async t => {
const keyv = new Keyv({store: new Map()});
await keyv.set('foo', 'bar');
Expand Down Expand Up @@ -341,14 +356,12 @@ test.it('keyv.get([keys]) should return undefined array for all no existent keys
});

test.it('pass compress options', async t => {
// @ts-expect-error - compression options
const keyv = new Keyv({store: new Map(), compression: new KeyvBrotli()});
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
});

test.it('compress/decompress with gzip', async t => {
// @ts-expect-error - compression options
const keyv = new Keyv({store: new Map(), compression: new KeyvGzip()});
await keyv.set('foo', 'bar');
t.expect(await keyv.get('foo')).toBe('bar');
Expand All @@ -363,7 +376,6 @@ test.it(
'keyv iterator() doesn\'t yield values from other namespaces with compression',
async t => {
const KeyvStore = new Map();
// @ts-expect-error - compression options
const keyv1 = new Keyv({store: KeyvStore, namespace: 'keyv1', compression: new KeyvGzip()});
const map1 = new Map(
Array.from({length: 5})
Expand All @@ -376,7 +388,6 @@ test.it(
}

await Promise.all(toResolve);
// @ts-expect-error - compression options
const keyv2 = new Keyv({store: KeyvStore, namespace: 'keyv2', compression: new KeyvGzip()});
const map2 = new Map(
Array.from({length: 5})
Expand Down
Loading