@eldoy/webdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 Vidar Eldøy
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # WebDB
2
+
3
+ Document database API backed by CouchDB, exposed through a Mongo-style client.
4
+
5
+ ### Installation
6
+ ```sh
7
+ npm i @eldoy/webdb
8
+ ````
9
+
10
+ ### Usage
11
+
12
+ ```js
13
+ var webdb = require('@eldoy/webdb')
14
+ var db = webdb('http://admin:mysecretpassword@localhost:5984')
15
+
16
+ // Create one
17
+ var doc = await db('user').create({ name: 'Heimdal' })
18
+
19
+ // Bulk insert
20
+ var n = await db('user').bulk([
21
+ { name: 'A' },
22
+ { name: 'B' }
23
+ ])
24
+
25
+ // Update one or many
26
+ var n = await db('user').update(
27
+ { name: 'A' },
28
+ { active: true }
29
+ )
30
+
31
+ // Get one (first match)
32
+ var doc = await db('user').get({ name: 'Heimdal' })
33
+
34
+ // Create indexes
35
+ await db('user').index([
36
+ ['name'],
37
+ ['name', 'email']
38
+ ])
39
+
40
+ // Find
41
+ var docs = await db('user').find({ name: 'Heimdal' })
42
+
43
+ // Find with sort
44
+ var docs = await db('user').find(
45
+ { name: 'Heimdal' },
46
+ { sort: [{ name: 'asc' }] }
47
+ )
48
+
49
+ // Find with limit
50
+ var docs = await db('user').find(
51
+ { name: 'Heimdal' },
52
+ { limit: 1 }
53
+ )
54
+
55
+ // Find with projection
56
+ var docs = await db('user').find(
57
+ { name: 'Heimdal' },
58
+ { fields: ['name', 'email'] }
59
+ )
60
+
61
+ // Delete one or many
62
+ var n = await db('user').delete({ active: false })
63
+
64
+ // Count
65
+ var n = await db('user').count({ name: 'Heimdal' })
66
+
67
+ // Batch processing (streamed pagination)
68
+ await db('user').batch(
69
+ { active: true },
70
+ { size: 500, sort: [{ created: 'asc' }] },
71
+ async function (docs) {
72
+ // handle a chunk
73
+ }
74
+ )
75
+
76
+ // Drop a specific database
77
+ await db('user').drop()
78
+
79
+ // Server-level compact
80
+ await db.compact('user')
81
+
82
+ // Drop all databases
83
+ await db.drop()
84
+ ```
85
+
86
+ ### Notes on Indexing, Sorting, and Mango Queries
87
+
88
+ **1. Selector fields and indexes**
89
+
90
+ Mango performs best when the selector matches an existing index.
91
+ Any field used in `{ selector: … }` benefits from being part of an index, but it is not required unless sorting is used.
92
+
93
+ **2. Sorting requires indexing**
94
+
95
+ Mango enforces that **every field in the sort must be indexed**.
96
+ Example:
97
+
98
+ ```js
99
+ await db('user').index([['age']])
100
+ await db('user').find({}, { sort: [{ age: 'asc' }] }) // valid
101
+ ```
102
+
103
+ Sorting without the correct index returns an error.
104
+
105
+ **3. Compound indexes**
106
+
107
+ An index like:
108
+
109
+ ```js
110
+ await db('user').index([['name', 'email']])
111
+ ```
112
+
113
+ supports selectors and sorts that use `name`, or `name` and `email` together, in the defined order.
114
+
115
+ **4. Projection (`fields`)**
116
+
117
+ Projection returns only selected fields.
118
+ Unindexed projection works fine; indexing does not affect `fields`.
119
+
120
+ **5. Pagination (batch)**
121
+
122
+ `batch()` uses Mango bookmarks internally.
123
+ It respects all options: `sort`, `limit`, `fields`, and `size`.
124
+
125
+ **6. Create-on-first-use**
126
+
127
+ Databases are auto-created when used. Explicit `drop()` allows clean-state tests.
128
+
129
+ **7. Null results**
130
+
131
+ `get()` returns `null` when no match exists.
132
+
133
+ ### ID note
134
+
135
+ CouchDB stores documents with `_id`, but write responses return the same value as `id`.
136
+ Use `doc.id` after `create()`, and `_id` for all queries and stored documents.
137
+
138
+ ### Acknowledgements
139
+
140
+ Created by [Tekki AS](https://tekki.no)
141
+
142
+ ISC Licensed.
package/index.js ADDED
@@ -0,0 +1,205 @@
1
+ var nano = require('nano')
2
+
3
+ module.exports = function (url) {
4
+ var server = nano(url)
5
+
6
+ var db = function (name) {
7
+ return api(name, server)
8
+ }
9
+
10
+ //
11
+ // SERVER-LEVEL OPS (drop all, compact specific)
12
+ //
13
+
14
+ db.drop = async function () {
15
+ var list = await server.db.list()
16
+ for (var i = 0; i < list.length; i++) {
17
+ await server.db.destroy(list[i])
18
+ }
19
+ }
20
+
21
+ db.compact = async function (name) {
22
+ await server.db.compact(name)
23
+ }
24
+
25
+ return db
26
+ }
27
+
28
+ //
29
+ // COLLECTION-LEVEL API (ordered to match test suite)
30
+ //
31
+
32
+ function api(name, server) {
33
+ return {
34
+ //
35
+ // CRUD
36
+ //
37
+
38
+ create: async function (doc) {
39
+ var db = await ensure(name, server)
40
+ return db.insert(doc)
41
+ },
42
+
43
+ bulk: async function (docs) {
44
+ var db = await ensure(name, server)
45
+ await db.bulk({ docs: docs })
46
+ return docs.length
47
+ },
48
+
49
+ update: async function (query, update) {
50
+ var db = await ensure(name, server)
51
+ var r = await db.find({ selector: query })
52
+ if (!r.docs.length) return 0
53
+
54
+ var out = []
55
+ for (var i = 0; i < r.docs.length; i++) {
56
+ var cur = r.docs[i]
57
+ var next = {}
58
+ for (var k in cur) next[k] = cur[k]
59
+ for (var k in update) next[k] = update[k]
60
+ out.push(next)
61
+ }
62
+
63
+ await db.bulk({ docs: out })
64
+ return out.length
65
+ },
66
+
67
+ get: async function (query) {
68
+ var dbi = await ensure(name, server)
69
+ var r = await dbi.find({ selector: query, limit: 1 })
70
+ return r.docs[0] || null
71
+ },
72
+
73
+ //
74
+ // FIND
75
+ //
76
+
77
+ find: async function (selector, opts) {
78
+ var dbi = await ensure(name, server)
79
+
80
+ var q = { selector: selector }
81
+ if (opts) {
82
+ if (opts.sort) q.sort = opts.sort
83
+ if (opts.limit) q.limit = opts.limit
84
+ if (opts.fields) q.fields = opts.fields
85
+ }
86
+
87
+ var r = await dbi.find(q)
88
+ return r.docs
89
+ },
90
+
91
+ //
92
+ // INDEX
93
+ //
94
+
95
+ index: async function (list) {
96
+ var dbi = await ensure(name, server)
97
+ for (var i = 0; i < list.length; i++) {
98
+ await dbi.createIndex({ index: { fields: list[i] } })
99
+ }
100
+ },
101
+
102
+ //
103
+ // DELETE
104
+ //
105
+
106
+ delete: async function (query) {
107
+ var dbi = await ensure(name, server)
108
+
109
+ var r = await dbi.find({ selector: query })
110
+ if (!r.docs.length) return 0
111
+
112
+ var out = []
113
+ for (var i = 0; i < r.docs.length; i++) {
114
+ var d = r.docs[i]
115
+ out.push({
116
+ _id: d._id,
117
+ _rev: d._rev,
118
+ _deleted: true
119
+ })
120
+ }
121
+
122
+ await dbi.bulk({ docs: out })
123
+ return out.length
124
+ },
125
+
126
+ //
127
+ // COUNT
128
+ //
129
+
130
+ count: async function (query) {
131
+ var dbi = await ensure(name, server)
132
+ var r = await dbi.find({ selector: query })
133
+ return r.docs.length
134
+ },
135
+
136
+ //
137
+ // DROP (collection-level)
138
+ //
139
+
140
+ drop: async function () {
141
+ try {
142
+ await server.db.destroy(name)
143
+ } catch (e) {}
144
+ },
145
+
146
+ //
147
+ // BATCH
148
+ //
149
+
150
+ batch: async function (query, opt, fn) {
151
+ var dbi = await ensure(name, server)
152
+
153
+ var size = opt && opt.size ? opt.size : 100
154
+ var limit = opt && opt.limit
155
+ var sort = opt && opt.sort
156
+ var fields = opt && opt.fields
157
+
158
+ var bookmark = null
159
+ var remaining = limit
160
+
161
+ for (;;) {
162
+ var effectiveSize = size
163
+ if (remaining && remaining < effectiveSize) {
164
+ effectiveSize = remaining
165
+ }
166
+
167
+ var q = {
168
+ selector: query,
169
+ limit: effectiveSize
170
+ }
171
+
172
+ if (sort) q.sort = sort
173
+ if (fields) q.fields = fields
174
+ if (bookmark) q.bookmark = bookmark
175
+
176
+ var r = await dbi.find(q)
177
+ var docs = r.docs
178
+ if (!docs.length) break
179
+
180
+ await fn(docs)
181
+
182
+ if (remaining) {
183
+ remaining -= docs.length
184
+ if (remaining <= 0) break
185
+ }
186
+
187
+ if (!r.bookmark || docs.length < effectiveSize) break
188
+ bookmark = r.bookmark
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ //
195
+ // ENSURE DB EXISTS
196
+ //
197
+
198
+ async function ensure(name, server) {
199
+ try {
200
+ await server.db.get(name)
201
+ } catch (e) {
202
+ await server.db.create(name)
203
+ }
204
+ return server.db.use(name)
205
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@eldoy/webdb",
3
+ "version": "0.1.0",
4
+ "description": "Document database client powered by CouchDB.",
5
+ "keywords": [
6
+ "database",
7
+ "couchdb",
8
+ "mango",
9
+ "document-store",
10
+ "nosql",
11
+ "json",
12
+ "client",
13
+ "db",
14
+ "nano",
15
+ "webdb"
16
+ ],
17
+ "scripts": {
18
+ "test": "spekk",
19
+ "test:watch": "nodemon --exec spekk"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/eldoy/webdb.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/eldoy/webdb/issues"
27
+ },
28
+ "homepage": "https://github.com/eldoy/webdb#readme",
29
+ "author": "Vidar Eldøy <vidar@eldoy.com>",
30
+ "license": "ISC",
31
+ "dependencies": {
32
+ "nano": "^11.0.3"
33
+ },
34
+ "devDependencies": {
35
+ "spekk": "^0.3.2"
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = async function () {
2
+ async function setup() {}
3
+
4
+ async function teardown() {}
5
+
6
+ return { db, setup, teardown }
7
+ }
@@ -0,0 +1,242 @@
1
+ var webdb = require('../../index.js')
2
+ var db = webdb('http://admin:mysecretpassword@localhost:5984')
3
+
4
+ beforeEach(async function ({ t }) {
5
+ await db.drop()
6
+ })
7
+
8
+ //
9
+ // CRUD: create, bulk, update, get
10
+ //
11
+
12
+ test('create', async function ({ t }) {
13
+ var doc = await db('user').create({ name: 'Heimdal' })
14
+ t.ok(doc && doc.id)
15
+ })
16
+
17
+ test('bulk', async function ({ t }) {
18
+ var n = await db('user').bulk([{ name: 'A' }, { name: 'B' }])
19
+ t.equal(n, 2)
20
+ })
21
+
22
+ test('update one', async function ({ t }) {
23
+ var doc = await db('user').create({ name: 'Old' })
24
+ var n = await db('user').update({ _id: doc.id }, { name: 'New' })
25
+ t.equal(n, 1)
26
+ })
27
+
28
+ test('update many', async function ({ t }) {
29
+ await db('user').bulk([{ role: 'x' }, { role: 'x' }, { role: 'y' }])
30
+ var n = await db('user').update({ role: 'x' }, { role: 'z' })
31
+ t.equal(n, 2)
32
+ })
33
+
34
+ test('get', async function ({ t }) {
35
+ await db('user').create({ name: 'Heimdal' })
36
+ var doc = await db('user').get({ name: 'Heimdal' })
37
+ t.ok(doc && doc._id)
38
+ })
39
+
40
+ //
41
+ // FIND: base, sort, limit, fields
42
+ //
43
+
44
+ test('find', async function ({ t }) {
45
+ await db('user').bulk([
46
+ { name: 'A', age: 1 },
47
+ { name: 'A', age: 2 },
48
+ { name: 'B', age: 3 }
49
+ ])
50
+
51
+ var docs = await db('user').find({ name: 'A' })
52
+ t.ok(Array.isArray(docs))
53
+ t.equal(docs.length, 2)
54
+ })
55
+
56
+ test('find sort', async function ({ t }) {
57
+ await db('user').index([['n']])
58
+
59
+ await db('user').bulk([
60
+ { name: 'A', n: 3 },
61
+ { name: 'A', n: 1 },
62
+ { name: 'A', n: 2 }
63
+ ])
64
+
65
+ var docs = await db('user').find({ name: 'A' }, { sort: [{ n: 'asc' }] })
66
+ t.equal(docs[0].n, 1)
67
+ t.equal(docs[2].n, 3)
68
+ })
69
+
70
+ test('find limit', async function ({ t }) {
71
+ await db('user').bulk([{ name: 'A' }, { name: 'A' }, { name: 'A' }])
72
+ var docs = await db('user').find({ name: 'A' }, { limit: 1 })
73
+ t.equal(docs.length, 1)
74
+ })
75
+
76
+ test('find fields', async function ({ t }) {
77
+ await db('user').bulk([{ name: 'A', age: 10 }])
78
+ var docs = await db('user').find({ name: 'A' }, { fields: ['name'] })
79
+ t.ok(docs[0].name)
80
+ t.equal(docs[0].age, undefined)
81
+ })
82
+
83
+ //
84
+ // INDEX
85
+ //
86
+
87
+ test('index', async function ({ t }) {
88
+ await db('user').index([['name', 'email']])
89
+ var docs = await db('user').find(
90
+ { name: 'X' },
91
+ { sort: [{ name: 'asc' }, { email: 'asc' }] }
92
+ )
93
+ t.ok(Array.isArray(docs))
94
+ })
95
+
96
+ //
97
+ // DELETE: one and many
98
+ //
99
+
100
+ test('delete one', async function ({ t }) {
101
+ var a = await db('user').create({ name: 'X' })
102
+ await db('user').create({ name: 'Y' })
103
+
104
+ var n = await db('user').delete({ _id: a.id })
105
+ t.equal(n, 1)
106
+
107
+ var doc = await db('user').get({ _id: a.id })
108
+ t.equal(doc, null)
109
+ })
110
+
111
+ test('delete many', async function ({ t }) {
112
+ await db('user').bulk([{ role: 'x' }, { role: 'x' }, { role: 'y' }])
113
+ var n = await db('user').delete({ role: 'x' })
114
+ t.equal(n, 2)
115
+
116
+ var docs = await db('user').find({ role: 'x' })
117
+ t.equal(docs.length, 0)
118
+ })
119
+
120
+ //
121
+ // COUNT
122
+ //
123
+
124
+ test('count', async function ({ t }) {
125
+ await db('user').bulk([{ type: 'a' }, { type: 'a' }, { type: 'b' }])
126
+ var n = await db('user').count({ type: 'a' })
127
+ t.equal(n, 2)
128
+ })
129
+
130
+ //
131
+ // BATCH: base + query + size + sort + limit + fields
132
+ //
133
+
134
+ test('batch', async function ({ t }) {
135
+ await db('user').bulk([{ v: 1 }, { v: 2 }, { v: 3 }, { v: 4 }])
136
+
137
+ var collected = []
138
+
139
+ await db('user').batch({}, { size: 2 }, async function (docs) {
140
+ for (var i = 0; i < docs.length; i++) collected.push(docs[i].v)
141
+ })
142
+
143
+ t.equal(collected.length, 4)
144
+ t.ok(collected.includes(1))
145
+ t.ok(collected.includes(4))
146
+ })
147
+
148
+ test('batch with query', async function ({ t }) {
149
+ await db('user').bulk([
150
+ { type: 'a', v: 1 },
151
+ { type: 'a', v: 2 },
152
+ { type: 'b', v: 3 }
153
+ ])
154
+
155
+ var out = []
156
+
157
+ await db('user').batch({ type: 'a' }, { size: 10 }, async function (docs) {
158
+ for (var i = 0; i < docs.length; i++) out.push(docs[i].v)
159
+ })
160
+
161
+ t.equal(out.length, 2)
162
+ t.ok(out.includes(1))
163
+ t.ok(out.includes(2))
164
+ })
165
+
166
+ test('batch with size', async function ({ t }) {
167
+ await db('user').bulk([{ n: 1 }, { n: 2 }, { n: 3 }, { n: 4 }])
168
+
169
+ var chunks = []
170
+
171
+ await db('user').batch({}, { size: 2 }, async function (docs) {
172
+ chunks.push(docs.length)
173
+ })
174
+
175
+ t.equal(chunks.length, 2)
176
+ t.equal(chunks[0], 2)
177
+ t.equal(chunks[1], 2)
178
+ })
179
+
180
+ test('batch respects sort', async function ({ t }) {
181
+ await db('user').index([['n']])
182
+
183
+ await db('user').bulk([{ n: 3 }, { n: 1 }, { n: 2 }])
184
+
185
+ var list = []
186
+
187
+ await db('user').batch(
188
+ {},
189
+ { size: 1, sort: [{ n: 'asc' }] },
190
+ async function (docs) {
191
+ list.push(docs[0].n)
192
+ }
193
+ )
194
+
195
+ t.deepEqual(list, [1, 2, 3])
196
+ })
197
+
198
+ test('batch respects limit', async function ({ t }) {
199
+ await db('user').bulk([{ n: 1 }, { n: 2 }, { n: 3 }])
200
+
201
+ var list = []
202
+
203
+ await db('user').batch({}, { limit: 2 }, async function (docs) {
204
+ for (var i = 0; i < docs.length; i++) list.push(docs[i].n)
205
+ })
206
+
207
+ t.equal(list.length, 2)
208
+ })
209
+
210
+ test('batch respects fields', async function ({ t }) {
211
+ await db('user').bulk([{ name: 'A', age: 10 }])
212
+
213
+ var fields = []
214
+
215
+ await db('user').batch(
216
+ { name: 'A' },
217
+ { fields: ['name'] },
218
+ async function (docs) {
219
+ fields.push(Object.keys(docs[0]))
220
+ }
221
+ )
222
+
223
+ t.deepEqual(fields[0], ['name'])
224
+ })
225
+
226
+ //
227
+ // DATABASE-LEVEL OPS
228
+ //
229
+
230
+ test('drop database', async function ({ t }) {
231
+ await db('user').create({ name: 'A' })
232
+ await db('user').drop()
233
+ var doc = await db('user').get({ name: 'A' })
234
+ t.equal(doc, null)
235
+ })
236
+
237
+ test('compact', async function ({ t }) {
238
+ await db('user').create({ name: 'A' })
239
+ await db('user').update({ name: 'A' }, { name: 'B' })
240
+ await db.compact('user')
241
+ t.ok(true)
242
+ })