Skip to content

Commit 6684039

Browse files
authored
Implement has() and hasMany() (#96)
Adds two methods: ```js await db.put('love', 'u') await db.has('love') // true await db.hasMany(['love', 'hate']) // [true, false] ``` Ref: Level/community#142 Category: addition
1 parent eedeed9 commit 6684039

File tree

9 files changed

+449
-1
lines changed

9 files changed

+449
-1
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,45 @@ Get multiple values from the database by an array of `keys`. The optional `optio
168168

169169
Returns a promise for an array of values with the same order as `keys`. If a key was not found, the relevant value will be `undefined`.
170170

171+
### `db.has(key[, options])`
172+
173+
Check if the database has an entry with the given `key`. The optional `options` object may contain:
174+
175+
- `keyEncoding`: custom key encoding for this operation, used to encode the `key`.
176+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
177+
178+
Returns a promise for a boolean. For example:
179+
180+
```js
181+
if (await db.has('fruit')) {
182+
console.log('We have fruit')
183+
}
184+
```
185+
186+
If the value of the entry is needed, instead do:
187+
188+
```js
189+
const value = await db.get('fruit')
190+
191+
if (value !== undefined) {
192+
console.log('We have fruit: %o', value)
193+
}
194+
```
195+
196+
### `db.hasMany(keys[, options])`
197+
198+
Check if the database has entries with the given keys. The `keys` argument must be an array. The optional `options` object may contain:
199+
200+
- `keyEncoding`: custom key encoding for this operation, used to encode the `keys`.
201+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
202+
203+
Returns a promise for an array of booleans with the same order as `keys`. For example:
204+
205+
```js
206+
await db.put('a', '123')
207+
await db.hasMany(['a', 'b']) // [true, false]
208+
```
209+
171210
### `db.put(key, value[, options])`
172211

173212
Add a new entry or overwrite an existing entry. The optional `options` object may contain:

abstract-level.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,117 @@ class AbstractLevel extends EventEmitter {
449449
return new Array(keys.length).fill(undefined)
450450
}
451451

452+
async has (key, options) {
453+
options = getOptions(options, this[kDefaultOptions].key)
454+
455+
if (this[kStatus] === 'opening') {
456+
return this.deferAsync(() => this.has(key, options))
457+
}
458+
459+
assertOpen(this)
460+
461+
// TODO (next major): change this to an assert
462+
const err = this._checkKey(key)
463+
if (err) throw err
464+
465+
const snapshot = options.snapshot != null ? options.snapshot : null
466+
const keyEncoding = this.keyEncoding(options.keyEncoding)
467+
const keyFormat = keyEncoding.format
468+
469+
// Forward encoding options to the underlying store
470+
if (options === this[kDefaultOptions].key) {
471+
// Avoid Object.assign() for default options
472+
options = this[kDefaultOptions].keyFormat
473+
} else if (options.keyEncoding !== keyFormat) {
474+
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
475+
options = Object.assign({}, options, { keyEncoding: keyFormat })
476+
}
477+
478+
const encodedKey = keyEncoding.encode(key)
479+
const mappedKey = this.prefixKey(encodedKey, keyFormat, true)
480+
481+
// Keep snapshot open during operation
482+
if (snapshot !== null) {
483+
snapshot.ref()
484+
}
485+
486+
try {
487+
return this._has(mappedKey, options)
488+
} finally {
489+
// Release snapshot
490+
if (snapshot !== null) {
491+
snapshot.unref()
492+
}
493+
}
494+
}
495+
496+
async _has (key, options) {
497+
throw new ModuleError('Database does not support has()', {
498+
code: 'LEVEL_NOT_SUPPORTED'
499+
})
500+
}
501+
502+
async hasMany (keys, options) {
503+
options = getOptions(options, this[kDefaultOptions].entry)
504+
505+
if (this[kStatus] === 'opening') {
506+
return this.deferAsync(() => this.hasMany(keys, options))
507+
}
508+
509+
assertOpen(this)
510+
511+
if (!Array.isArray(keys)) {
512+
throw new TypeError("The first argument 'keys' must be an array")
513+
}
514+
515+
if (keys.length === 0) {
516+
return []
517+
}
518+
519+
const snapshot = options.snapshot != null ? options.snapshot : null
520+
const keyEncoding = this.keyEncoding(options.keyEncoding)
521+
const keyFormat = keyEncoding.format
522+
523+
// Forward encoding options to the underlying store
524+
if (options === this[kDefaultOptions].key) {
525+
// Avoid Object.assign() for default options
526+
options = this[kDefaultOptions].keyFormat
527+
} else if (options.keyEncoding !== keyFormat) {
528+
// Avoid spread operator because of https://bugs.chromium.org/p/chromium/issues/detail?id=1204540
529+
options = Object.assign({}, options, { keyEncoding: keyFormat })
530+
}
531+
532+
const mappedKeys = new Array(keys.length)
533+
534+
for (let i = 0; i < keys.length; i++) {
535+
const key = keys[i]
536+
const err = this._checkKey(key)
537+
if (err) throw err
538+
539+
mappedKeys[i] = this.prefixKey(keyEncoding.encode(key), keyFormat, true)
540+
}
541+
542+
// Keep snapshot open during operation
543+
if (snapshot !== null) {
544+
snapshot.ref()
545+
}
546+
547+
try {
548+
return this._hasMany(mappedKeys, options)
549+
} finally {
550+
// Release snapshot
551+
if (snapshot !== null) {
552+
snapshot.unref()
553+
}
554+
}
555+
}
556+
557+
async _hasMany (keys, options) {
558+
throw new ModuleError('Database does not support hasMany()', {
559+
code: 'LEVEL_NOT_SUPPORTED'
560+
})
561+
}
562+
452563
async put (key, value, options) {
453564
if (!this.hooks.prewrite.noop) {
454565
// Forward to batch() which will run the hook

lib/abstract-sublevel.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ module.exports = function ({ AbstractLevel }) {
146146
return this[kParent].getMany(keys, options)
147147
}
148148

149+
async _has (key, options) {
150+
return this[kParent].has(key, options)
151+
}
152+
153+
async _hasMany (keys, options) {
154+
return this[kParent].hasMany(keys, options)
155+
}
156+
149157
async _del (key, options) {
150158
return this[kParent].del(key, options)
151159
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"dependencies": {
2929
"buffer": "^6.0.3",
3030
"is-buffer": "^2.0.5",
31-
"level-supports": "^6.1.1",
31+
"level-supports": "^6.2.0",
3232
"level-transcoder": "^1.0.1",
3333
"maybe-combine-errors": "^1.0.0",
3434
"module-error": "^1.0.1"

test/has-many-test.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict'
2+
3+
const { illegalKeys } = require('./util')
4+
const traits = require('./traits')
5+
6+
let db
7+
8+
/**
9+
* @param {import('tape')} test
10+
*/
11+
exports.setUp = function (test, testCommon) {
12+
test('hasMany() setup', async function (t) {
13+
db = testCommon.factory()
14+
return db.open()
15+
})
16+
}
17+
18+
/**
19+
* @param {import('tape')} test
20+
*/
21+
exports.args = function (test, testCommon) {
22+
test('hasMany() requires an array argument', function (t) {
23+
t.plan(6)
24+
25+
db.hasMany().catch(function (err) {
26+
t.is(err && err.name, 'TypeError')
27+
t.is(err && err.message, "The first argument 'keys' must be an array")
28+
})
29+
30+
db.hasMany('foo').catch(function (err) {
31+
t.is(err && err.name, 'TypeError')
32+
t.is(err && err.message, "The first argument 'keys' must be an array")
33+
})
34+
35+
db.hasMany('foo', {}).catch(function (err) {
36+
t.is(err && err.name, 'TypeError')
37+
t.is(err && err.message, "The first argument 'keys' must be an array")
38+
})
39+
})
40+
41+
test('hasMany() with illegal keys', function (t) {
42+
t.plan(illegalKeys.length * 4)
43+
44+
for (const { name, key } of illegalKeys) {
45+
db.hasMany([key]).catch(function (err) {
46+
t.ok(err instanceof Error, name + ' - is Error')
47+
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code')
48+
})
49+
50+
db.hasMany(['valid', key]).catch(function (err) {
51+
t.ok(err instanceof Error, name + ' - is Error (second key)')
52+
t.is(err.code, 'LEVEL_INVALID_KEY', name + ' - correct error code (second key)')
53+
})
54+
}
55+
})
56+
}
57+
58+
/**
59+
* @param {import('tape')} test
60+
*/
61+
exports.hasMany = function (test, testCommon) {
62+
test('simple hasMany()', async function (t) {
63+
await db.put('foo', 'bar')
64+
65+
t.same(await db.hasMany(['foo']), [true])
66+
t.same(await db.hasMany(['foo'], {}), [true]) // same but with {}
67+
t.same(await db.hasMany(['beep']), [false])
68+
69+
await db.put('beep', 'boop')
70+
71+
t.same(await db.hasMany(['beep']), [true])
72+
t.same(await db.hasMany(['foo', 'beep']), [true, true])
73+
t.same(await db.hasMany(['aaa', 'beep']), [false, true])
74+
t.same(await db.hasMany(['beep', 'aaa']), [true, false], 'maintains order of input keys')
75+
})
76+
77+
test('empty hasMany()', async function (t) {
78+
t.same(await db.hasMany([]), [])
79+
80+
const encodings = Object.keys(db.supports.encodings)
81+
.filter(k => db.supports.encodings[k])
82+
83+
for (const valueEncoding of encodings) {
84+
t.same(await db.hasMany([], { valueEncoding }), [])
85+
}
86+
})
87+
88+
test('simultaneous hasMany()', async function (t) {
89+
t.plan(20)
90+
91+
await db.put('hello', 'world')
92+
const promises = []
93+
94+
for (let i = 0; i < 10; ++i) {
95+
promises.push(db.hasMany(['hello']).then(function (values) {
96+
t.same(values, [true])
97+
}))
98+
}
99+
100+
for (let i = 0; i < 10; ++i) {
101+
promises.push(db.hasMany(['non-existent']).then(function (values) {
102+
t.same(values, [false])
103+
}))
104+
}
105+
106+
return Promise.all(promises)
107+
})
108+
109+
traits.open('hasMany()', testCommon, async function (t, db) {
110+
t.same(await db.hasMany(['foo']), [false])
111+
})
112+
113+
traits.closed('hasMany()', testCommon, async function (t, db) {
114+
return db.hasMany(['foo'])
115+
})
116+
117+
// Also test empty array because it has a fast-path
118+
traits.open('hasMany() with empty array', testCommon, async function (t, db) {
119+
t.same(await db.hasMany([]), [])
120+
})
121+
122+
traits.closed('hasMany() with empty array', testCommon, async function (t, db) {
123+
return db.hasMany([])
124+
})
125+
}
126+
127+
/**
128+
* @param {import('tape')} test
129+
*/
130+
exports.tearDown = function (test, testCommon) {
131+
test('hasMany() teardown', async function (t) {
132+
return db.close()
133+
})
134+
}
135+
136+
/**
137+
* @param {import('tape')} test
138+
*/
139+
exports.all = function (test, testCommon) {
140+
exports.setUp(test, testCommon)
141+
exports.args(test, testCommon)
142+
exports.hasMany(test, testCommon)
143+
exports.tearDown(test, testCommon)
144+
}

0 commit comments

Comments
 (0)