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 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
@@ -0,0 +1,6 @@
1
+ import { bulkGet, bulkSave, bulkRemove } from './impl/bulk'
2
+ import { get, put } from './impl/crud'
3
+ import { query } from './impl/query'
4
+
5
+ export { bulkGet, bulkSave, bulkRemove, get, put, query }
6
+
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
+ }
@@ -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
+ )
@@ -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))
@@ -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))