hide-a-bed 1.0.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/README.md +73 -0
- package/impl/bulk.mjs +44 -0
- package/impl/crud.mjs +34 -0
- package/impl/query.mjs +45 -0
- package/index.mjs +6 -0
- package/package.json +27 -0
- package/schema/bulk.mjs +28 -0
- package/schema/crud.mjs +28 -0
- package/schema/query.mjs +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
hide-a-bed
|
|
2
|
+
-----------
|
|
3
|
+
|
|
4
|
+
A simple way to abstract couchdb, and make your interface to the database testable.
|
|
5
|
+
|
|
6
|
+
Install
|
|
7
|
+
-----
|
|
8
|
+
|
|
9
|
+
There are two packages, one for runtime that contains the real implementations and schema, and the other contains the stubs for tests.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npm i hide-a-bed --save
|
|
13
|
+
npm i hide-a-bed-stub --save-dev
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Code that uses some example db apis
|
|
18
|
+
```
|
|
19
|
+
export function doStuff (config, services, id) {
|
|
20
|
+
const doc = await services.db.get(config, id)
|
|
21
|
+
const apiResult = services.callSomeApi(config, doc.userName)
|
|
22
|
+
const query = {
|
|
23
|
+
startkey: apiResult.startTime,
|
|
24
|
+
endkey: apiResult.endTime
|
|
25
|
+
}
|
|
26
|
+
const queryResults = await db.query(config, '_design/userThings/_view/byTime', query)
|
|
27
|
+
return queryResults.rows
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Using doStuff, in a real env, connecting to a real couch
|
|
33
|
+
```
|
|
34
|
+
import db from 'hide-a-bed'
|
|
35
|
+
import { doStuff } from './doStuff'
|
|
36
|
+
import { callSomeApi } from './api'
|
|
37
|
+
// the config object needs a couch url
|
|
38
|
+
const config = { couch: 'http://localhost:5984/mydb' }
|
|
39
|
+
// build up a service api for all your external calls that can be mocked/stubbed
|
|
40
|
+
const services = { db, callSomeApi }
|
|
41
|
+
const afterStuff = await doStuff(config, services, 'happy-doc-id')
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Mocking out the calls in a test, never connects to the network
|
|
46
|
+
```
|
|
47
|
+
import { setup } from 'hide-a-bed-stub' // different package, since installed with --save-dev reduces space
|
|
48
|
+
import { doStuff } from './doStuff'
|
|
49
|
+
import { callSomeApiMock } from './test/mock/api'
|
|
50
|
+
// the config object needs a couch url, prove to yourself that its mocked with a fakeurl
|
|
51
|
+
const config = { couch: 'http://fakeurl:5984/mydb' }
|
|
52
|
+
|
|
53
|
+
// we import or design docs that we will need for the db
|
|
54
|
+
import userThingsDesignDoc from './ddocs/userThingsDDoc.js'
|
|
55
|
+
|
|
56
|
+
test('doStuff works in stub mode', async t => {
|
|
57
|
+
// we have to setup the db with the design docs that are required
|
|
58
|
+
const db = await setup([userThingsDesignDoc])
|
|
59
|
+
|
|
60
|
+
// build up a service api with all your fake endpoints
|
|
61
|
+
const services = { db, callSomeApi: callSomeApiMock }
|
|
62
|
+
const afterStuff = await doStuff(config, services, 'happy-doc-id')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Below are all the couch apis available
|
|
68
|
+
-------------
|
|
69
|
+
|
|
70
|
+
__TODO__
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
package/impl/bulk.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import needle from 'needle'
|
|
3
|
+
|
|
4
|
+
const opts = {
|
|
5
|
+
json: true,
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function bulkSave (config, docs) {
|
|
12
|
+
if (!docs) return
|
|
13
|
+
if (!docs.length) return
|
|
14
|
+
|
|
15
|
+
const url = `${config.couch}/_bulk_docs`
|
|
16
|
+
const body = { docs }
|
|
17
|
+
const resp = await needle('post', url, body, opts)
|
|
18
|
+
if (resp.statusCode !== 201) throw new Error('could not save')
|
|
19
|
+
const results = resp?.body || []
|
|
20
|
+
return results
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function bulkGet (config, ids) {
|
|
24
|
+
const keys = ids
|
|
25
|
+
const url = `${config.couch}/_all_docs?include_docs=true`
|
|
26
|
+
const body = { keys }
|
|
27
|
+
const resp = await needle('post', url, body, opts)
|
|
28
|
+
if (resp.statusCode !== 200) throw new Error('could not fetch')
|
|
29
|
+
const rows = resp?.body?.rows || []
|
|
30
|
+
const docs = []
|
|
31
|
+
rows.forEach(r => {
|
|
32
|
+
if (r.error) return
|
|
33
|
+
if (!r.key) return
|
|
34
|
+
if (!r.doc) return
|
|
35
|
+
docs.push(r.doc)
|
|
36
|
+
})
|
|
37
|
+
return docs
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function bulkRemove (config, ids) {
|
|
41
|
+
const docs = await bulkGet(config, ids)
|
|
42
|
+
docs.forEach(d => { d._deleted = true })
|
|
43
|
+
return bulkSave(config, docs)
|
|
44
|
+
}
|
package/impl/crud.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import needle from 'needle'
|
|
3
|
+
import { CouchGet, CouchPut } from '../schema/crud.mjs'
|
|
4
|
+
|
|
5
|
+
const opts = {
|
|
6
|
+
json: true,
|
|
7
|
+
headers: {
|
|
8
|
+
'Content-Type': 'application/json'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const get = CouchGet.implement(async (config, id) => {
|
|
13
|
+
const url = `${config.couch}/${id}`
|
|
14
|
+
const resp = await needle('get', url, opts)
|
|
15
|
+
const result = resp?.body || {}
|
|
16
|
+
result.statusCode = resp.statusCode
|
|
17
|
+
if (resp.statusCode !== 200) {
|
|
18
|
+
throw new Error('not found')
|
|
19
|
+
}
|
|
20
|
+
return result
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const put = CouchPut.implement(async (config, doc) => {
|
|
24
|
+
const url = `${config.couch}/${doc._id}`
|
|
25
|
+
const body = doc
|
|
26
|
+
const resp = await needle('put', url, body, opts)
|
|
27
|
+
const result = resp?.body || {}
|
|
28
|
+
result.statusCode = resp.statusCode
|
|
29
|
+
if (resp.statusCode === 409) {
|
|
30
|
+
result.ok = false
|
|
31
|
+
result.error = 'conflict'
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
})
|
package/impl/query.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod' // eslint-disable-line
|
|
4
|
+
import needle from 'needle'
|
|
5
|
+
import { SimpleViewQuery, SimpleViewQueryResponse } from '../schema/query.mjs' // eslint-disable-line
|
|
6
|
+
|
|
7
|
+
import pkg from 'lodash'
|
|
8
|
+
const { includes } = pkg
|
|
9
|
+
|
|
10
|
+
/** @type { z.infer<SimpleViewQuery> } query */
|
|
11
|
+
export const query = SimpleViewQuery.implement(async (config, view, options) => {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
const qs = queryString(options, ['key', 'startkey', 'endkey', 'reduce', 'group', 'group_level'])
|
|
14
|
+
|
|
15
|
+
const opts = {
|
|
16
|
+
json: true,
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const url = `${config.couch}/${view}?${qs.toString()}`
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
const results = await needle('get', url, opts)
|
|
24
|
+
/** @type { z.infer<SimpleViewQueryResponse> } body */
|
|
25
|
+
const body = results.body
|
|
26
|
+
if (body.error) throw new Error(body.error)
|
|
27
|
+
return body
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function queryString (options, params) {
|
|
31
|
+
const parts = Object.keys(options).map(key => {
|
|
32
|
+
let value = options[key]
|
|
33
|
+
if (includes(params, key)) {
|
|
34
|
+
if (typeof value === 'string') value = `"${value}"`
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
value = '[' + value.map(i => {
|
|
37
|
+
if (typeof i === 'string') return `"${i}"`
|
|
38
|
+
return i
|
|
39
|
+
}).join(',') + ']'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return `${key}=${value}`
|
|
43
|
+
})
|
|
44
|
+
return parts.join('&')
|
|
45
|
+
}
|
package/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hide-a-bed",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An abstraction over couchdb calls that includes easy mock/stubs with pouchdb",
|
|
5
|
+
"main": "index.mjs",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ryanramage/hide-a-bed.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"couchdb",
|
|
15
|
+
"test"
|
|
16
|
+
],
|
|
17
|
+
"author": "ryan ramage",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ryanramage/hide-a-bed/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ryanramage/hide-a-bed#readme",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"needle": "^3.2.0",
|
|
25
|
+
"zod": "^3.22.4"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/schema/bulk.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// TODO - type this object
|
|
4
|
+
export const SaveResponseSchema = z.array(z.object({
|
|
5
|
+
ok: z.boolean().nullish(),
|
|
6
|
+
id: z.string(),
|
|
7
|
+
rev: z.string().nullish(),
|
|
8
|
+
error: z.string().nullish().describe('if an error occured, one word reason, eg conflict'),
|
|
9
|
+
reason: z.string().nullish().describe('a full error message')
|
|
10
|
+
}))
|
|
11
|
+
/** @typedef { z.infer<typeof SaveResponseSchema> } Response */
|
|
12
|
+
|
|
13
|
+
export const BulkSave = z.function().args(
|
|
14
|
+
z.object({
|
|
15
|
+
couch: z.string().describe('the url to the couch database')
|
|
16
|
+
}).passthrough().describe('config object'),
|
|
17
|
+
z.array(z.object({
|
|
18
|
+
_id: z.string()
|
|
19
|
+
}).passthrough())
|
|
20
|
+
).returns(z.promise(SaveResponseSchema))
|
|
21
|
+
/** @typedef { z.infer<typeof SaveSchema> } Save */
|
|
22
|
+
|
|
23
|
+
export const BulkGet = z.function().args(
|
|
24
|
+
z.object({
|
|
25
|
+
couch: z.string().describe('the url to the couch database')
|
|
26
|
+
}).passthrough().describe('config object'),
|
|
27
|
+
z.array(z.string().describe('the ids to get'))
|
|
28
|
+
)
|
package/schema/crud.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const CouchDoc = z.object({
|
|
4
|
+
_id: z.string().describe('the couch doc id'),
|
|
5
|
+
_rev: z.string().optional().describe('the doc revision')
|
|
6
|
+
}).passthrough()
|
|
7
|
+
|
|
8
|
+
export const CouchDocResponse = z.object({
|
|
9
|
+
ok: z.boolean().optional().describe('did the request succeed'),
|
|
10
|
+
error: z.string().optional().describe('the error message, if did not succed'),
|
|
11
|
+
statusCode: z.number(),
|
|
12
|
+
id: z.string().optional().describe('the couch doc id'),
|
|
13
|
+
rev: z.string().optional().describe('the new rev of the doc')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const CouchConfig = z.object({
|
|
17
|
+
couch: z.string().describe('the url of the couch db')
|
|
18
|
+
}).passthrough().describe('The std config object')
|
|
19
|
+
|
|
20
|
+
export const CouchPut = z.function().args(
|
|
21
|
+
CouchConfig,
|
|
22
|
+
CouchDoc
|
|
23
|
+
).returns(z.promise(CouchDocResponse))
|
|
24
|
+
|
|
25
|
+
export const CouchGet = z.function().args(
|
|
26
|
+
CouchConfig,
|
|
27
|
+
z.string().describe('the couch doc id')
|
|
28
|
+
).returns(z.promise(CouchDoc))
|
package/schema/query.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const SimpleViewQueryResponse = z.object({
|
|
4
|
+
error: z.string().optional().describe('if something is wrong'),
|
|
5
|
+
rows: z.array(z.object({
|
|
6
|
+
id: z.string().optional(),
|
|
7
|
+
key: z.any().nullable(),
|
|
8
|
+
value: z.any().nullable(),
|
|
9
|
+
doc: z.object({}).passthrough().optional()
|
|
10
|
+
}))
|
|
11
|
+
}).passthrough()
|
|
12
|
+
|
|
13
|
+
export const SimpleViewQueryConfig = z.object({
|
|
14
|
+
couch: z.string().describe('the url of the couch db')
|
|
15
|
+
}).passthrough().describe('The std config object')
|
|
16
|
+
|
|
17
|
+
export const SimpleViewQuery = z.function().args(
|
|
18
|
+
SimpleViewQueryConfig,
|
|
19
|
+
z.string().describe('the view name'),
|
|
20
|
+
z.object({
|
|
21
|
+
startkey: z.any().optional(),
|
|
22
|
+
endkey: z.any().optional(),
|
|
23
|
+
descending: z.boolean().optional().describe('sort results descending'),
|
|
24
|
+
skip: z.number().positive().optional().describe('skip this many rows'),
|
|
25
|
+
limit: z.number().positive().optional().describe('limit the results to this many rows'),
|
|
26
|
+
key: z.any().optional(),
|
|
27
|
+
include_docs: z.boolean().optional().describe('join the id to the doc and return it'),
|
|
28
|
+
reduce: z.boolean().optional().describe('reduce the results'),
|
|
29
|
+
group: z.boolean().optional().describe('group the results'),
|
|
30
|
+
group_level: z.number().positive().optional().describe('group the results at this level')
|
|
31
|
+
}).optional().describe('query options')
|
|
32
|
+
).returns(z.promise(SimpleViewQueryResponse))
|