@eldoy/webdb 0.1.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +138 -1
  2. package/index.js +106 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ Document database API backed by CouchDB, exposed through a Mongo-style client.
5
5
  ### Installation
6
6
  ```sh
7
7
  npm i @eldoy/webdb
8
- ````
8
+ ```
9
9
 
10
10
  ### Usage
11
11
 
@@ -28,6 +28,23 @@ var n = await db('user').update(
28
28
  { active: true }
29
29
  )
30
30
 
31
+ // Set (update exactly one)
32
+ var updated = await db('user').set(
33
+ { name: 'A' },
34
+ { name: 'B', active: true }
35
+ )
36
+
37
+ // Remove (delete exactly one)
38
+ var removed = await db('user').remove(
39
+ { name: 'B' }
40
+ )
41
+
42
+ // Upsert (create or update one)
43
+ var doc = await db('user').upsert(
44
+ { email: 'a@example.com' },
45
+ { email: 'a@example.com', name: 'Alice' }
46
+ )
47
+
31
48
  // Get one (first match)
32
49
  var doc = await db('user').get({ name: 'Heimdal' })
33
50
 
@@ -83,6 +100,126 @@ await db.compact('user')
83
100
  await db.drop()
84
101
  ```
85
102
 
103
+ ### Mango Query Options
104
+
105
+ WebDB queries map directly to CouchDB Mango selectors. All Mango operators and options work as expected through `find()` and `batch()`.
106
+
107
+ #### Comparison Operators
108
+ Mango supports standard comparison operators inside selectors:
109
+
110
+ ```
111
+ $eq equal
112
+ $ne not equal
113
+ $gt greater than
114
+ $gte greater than or equal
115
+ $lt less than
116
+ $lte less than or equal
117
+ $regex regular expression matching
118
+ ```
119
+
120
+ Example:
121
+
122
+ ```js
123
+ await db('user').find({
124
+ age: { $gte: 18 }
125
+ })
126
+ ```
127
+
128
+ #### Logical Operators
129
+
130
+ Combine conditions using logical operators:
131
+
132
+ ```
133
+ $and
134
+ $or
135
+ $not
136
+ $nor
137
+ ```
138
+
139
+ Example:
140
+
141
+ ```js
142
+ await db('user').find({
143
+ $or: [{ role: 'admin' }, { active: true }]
144
+ })
145
+ ```
146
+
147
+ #### Sorting
148
+
149
+ Sorting requires an index on every field in the sort specification:
150
+
151
+ ```js
152
+ await db('user').index([['created']])
153
+
154
+ await db('user').find(
155
+ {},
156
+ { sort: [{ created: 'asc' }] }
157
+ )
158
+ ```
159
+
160
+ #### Limiting
161
+
162
+ Limit returned documents:
163
+
164
+ ```js
165
+ await db('user').find({}, { limit: 10 })
166
+ ```
167
+
168
+ #### Projection (fields)
169
+
170
+ Return only selected fields:
171
+
172
+ ```js
173
+ await db('user').find(
174
+ {},
175
+ { fields: ['name', 'email'] }
176
+ )
177
+ ```
178
+
179
+ #### Date Handling
180
+
181
+ CouchDB stores dates as strings.
182
+ Using ISO-8601 timestamps (`new Date().toISOString()`) enables:
183
+
184
+ * correct lexical comparison
185
+ * correct sorting
186
+ * correct `$gt` / `$lt` range queries
187
+
188
+ Example:
189
+
190
+ ```js
191
+ await db('log').find({
192
+ created: { $gt: "2024-01-01T00:00:00.000Z" }
193
+ })
194
+ ```
195
+
196
+ ISO strings compare and sort in true chronological order.
197
+
198
+ #### Batch Queries
199
+
200
+ `batch()` supports all Mango options:
201
+
202
+ * `size` (page size)
203
+ * `limit`
204
+ * `sort`
205
+ * `fields`
206
+ * standard selectors
207
+
208
+ Example:
209
+
210
+ ```js
211
+ await db('user').batch(
212
+ { created: { $gt: cutoff } },
213
+ { size: 100, sort: [{ created: 'asc' }] },
214
+ async function (docs) {
215
+ // process chunk
216
+ }
217
+ )
218
+ ```
219
+
220
+ All Mango selectors and options work identically in both `find()` and `batch()`.
221
+
222
+
86
223
  ### Notes on Indexing, Sorting, and Mango Queries
87
224
 
88
225
  **1. Selector fields and indexes**
package/index.js CHANGED
@@ -46,6 +46,39 @@ function api(name, server) {
46
46
  return docs.length
47
47
  },
48
48
 
49
+ upsert: async function (query, update) {
50
+ var dbi = await ensure(name, server)
51
+
52
+ // find one match
53
+ var r = await dbi.find({
54
+ selector: query,
55
+ limit: 1
56
+ })
57
+
58
+ // create if not found
59
+ if (!r.docs.length) {
60
+ var created = await dbi.insert(update)
61
+ return {
62
+ _id: created.id,
63
+ _rev: created.rev,
64
+ ...update
65
+ }
66
+ }
67
+
68
+ // update existing
69
+ var cur = r.docs[0]
70
+ var next = {}
71
+
72
+ for (var k in cur) next[k] = cur[k]
73
+ for (var k in update) next[k] = update[k]
74
+
75
+ var res = await dbi.insert(next)
76
+
77
+ next._id = res.id
78
+ next._rev = res.rev
79
+ return next
80
+ },
81
+
49
82
  update: async function (query, update) {
50
83
  var db = await ensure(name, server)
51
84
  var r = await db.find({ selector: query })
@@ -70,39 +103,59 @@ function api(name, server) {
70
103
  return r.docs[0] || null
71
104
  },
72
105
 
73
- //
74
- // FIND
75
- //
76
-
77
- find: async function (selector, opts) {
106
+ set: async function (query, update) {
78
107
  var dbi = await ensure(name, server)
79
108
 
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
- }
109
+ // find one
110
+ var r = await dbi.find({
111
+ selector: query,
112
+ limit: 1
113
+ })
86
114
 
87
- var r = await dbi.find(q)
88
- return r.docs
89
- },
115
+ if (!r.docs.length) return null
90
116
 
91
- //
92
- // INDEX
93
- //
117
+ var cur = r.docs[0]
118
+ var next = {}
94
119
 
95
- index: async function (list) {
120
+ // merge current doc + update
121
+ for (var k in cur) next[k] = cur[k]
122
+ for (var k in update) next[k] = update[k]
123
+
124
+ // write updated doc
125
+ var res = await dbi.insert(next)
126
+
127
+ // return updated document shape
128
+ next._id = res.id
129
+ next._rev = res.rev
130
+ return next
131
+ },
132
+
133
+ remove: async function (query) {
96
134
  var dbi = await ensure(name, server)
97
- for (var i = 0; i < list.length; i++) {
98
- await dbi.createIndex({ index: { fields: list[i] } })
135
+
136
+ // find one
137
+ var r = await dbi.find({
138
+ selector: query,
139
+ limit: 1
140
+ })
141
+
142
+ if (!r.docs.length) return null
143
+
144
+ var doc = r.docs[0]
145
+
146
+ // mark as deleted
147
+ var res = await dbi.insert({
148
+ _id: doc._id,
149
+ _rev: doc._rev,
150
+ _deleted: true
151
+ })
152
+
153
+ return {
154
+ _id: res.id,
155
+ _rev: res.rev
99
156
  }
100
157
  },
101
158
 
102
- //
103
- // DELETE
104
- //
105
-
106
159
  delete: async function (query) {
107
160
  var dbi = await ensure(name, server)
108
161
 
@@ -123,6 +176,35 @@ function api(name, server) {
123
176
  return out.length
124
177
  },
125
178
 
179
+ //
180
+ // FIND
181
+ //
182
+
183
+ find: async function (query, opts) {
184
+ var dbi = await ensure(name, server)
185
+
186
+ var q = { selector: query }
187
+ if (opts) {
188
+ if (opts.sort) q.sort = opts.sort
189
+ if (opts.limit) q.limit = opts.limit
190
+ if (opts.fields) q.fields = opts.fields
191
+ }
192
+
193
+ var r = await dbi.find(q)
194
+ return r.docs
195
+ },
196
+
197
+ //
198
+ // INDEX
199
+ //
200
+
201
+ index: async function (list) {
202
+ var dbi = await ensure(name, server)
203
+ for (var i = 0; i < list.length; i++) {
204
+ await dbi.createIndex({ index: { fields: list[i] } })
205
+ }
206
+ },
207
+
126
208
  //
127
209
  // COUNT
128
210
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eldoy/webdb",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Document database client powered by CouchDB.",
5
5
  "publishConfig": {
6
6
  "access": "public"