hide-a-bed 3.0.1 → 4.0.1
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 +104 -0
- package/cjs/impl/bulk.cjs +55 -6
- package/cjs/impl/crud.cjs +57 -7
- package/cjs/impl/errors.cjs +63 -0
- package/cjs/impl/logger.cjs +61 -0
- package/cjs/impl/patch.cjs +10 -5
- package/cjs/impl/query.cjs +11 -1
- package/cjs/impl/retry.cjs +54 -0
- package/cjs/impl/stream.cjs +48 -7
- package/cjs/index.cjs +26 -2
- package/cjs/schema/bind.cjs +40 -0
- package/cjs/schema/bulk.cjs +14 -0
- package/cjs/schema/config.cjs +20 -1
- package/cjs/schema/crud.cjs +9 -1
- package/cjs/schema/patch.cjs +6 -6
- package/cjs/schema/query.cjs +28 -19
- package/cjs/schema/stream.cjs +42 -0
- package/impl/bulk.mjs +58 -6
- package/impl/crud.mjs +67 -6
- package/impl/errors.mjs +53 -0
- package/impl/logger.mjs +59 -0
- package/impl/patch.mjs +36 -4
- package/impl/query.mjs +38 -2
- package/impl/retry.mjs +39 -0
- package/impl/stream.mjs +72 -17
- package/index.mjs +34 -4
- package/package.json +1 -1
- package/schema/bind.mjs +21 -0
- package/schema/bulk.mjs +17 -0
- package/schema/config.mjs +22 -1
- package/schema/crud.mjs +12 -0
- package/schema/patch.mjs +10 -6
- package/schema/query.mjs +30 -18
- package/schema/stream.mjs +23 -0
- package/build/build.mjs +0 -16
- package/build/build.rewrite-imports.mjs +0 -14
- package/impl/bulk.d.mts +0 -7
- package/impl/bulk.d.mts.map +0 -1
- package/impl/crud.d.mts +0 -10
- package/impl/crud.d.mts.map +0 -1
- package/impl/patch.d.mts +0 -2
- package/impl/patch.d.mts.map +0 -1
- package/impl/query.d.mts +0 -104
- package/impl/query.d.mts.map +0 -1
- package/impl/stream.d.mts +0 -2
- package/impl/stream.d.mts.map +0 -1
- package/index.d.mts +0 -34
- package/index.d.mts.map +0 -1
- package/schema/bulk.d.mts +0 -100
- package/schema/bulk.d.mts.map +0 -1
- package/schema/config.d.mts +0 -9
- package/schema/config.d.mts.map +0 -1
- package/schema/crud.d.mts +0 -83
- package/schema/crud.d.mts.map +0 -1
- package/schema/patch.d.mts +0 -47
- package/schema/patch.d.mts.map +0 -1
- package/schema/query.d.mts +0 -151
- package/schema/query.d.mts.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var bind_exports = {};
|
|
20
|
+
__export(bind_exports, {
|
|
21
|
+
Bind: () => Bind
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(bind_exports);
|
|
24
|
+
var import_zod = require("zod");
|
|
25
|
+
var import_config = require("./config.cjs");
|
|
26
|
+
var import_bulk = require("./bulk.cjs");
|
|
27
|
+
var import_crud = require("./crud.cjs");
|
|
28
|
+
var import_patch = require("./patch.cjs");
|
|
29
|
+
var import_query = require("./query.cjs");
|
|
30
|
+
var import_stream = require("./stream.cjs");
|
|
31
|
+
const BindReturns = import_zod.z.object({
|
|
32
|
+
bulkGet: import_bulk.BulkGetBound,
|
|
33
|
+
bulkSave: import_bulk.BulkSaveBound,
|
|
34
|
+
get: import_crud.CouchGetBound,
|
|
35
|
+
put: import_crud.CouchPutBound,
|
|
36
|
+
patch: import_patch.PatchBound,
|
|
37
|
+
query: import_query.SimpleViewQueryBound,
|
|
38
|
+
queryStream: import_stream.SimpleViewQueryStreamBound
|
|
39
|
+
});
|
|
40
|
+
const Bind = import_zod.z.function().args(import_config.CouchConfig).returns(BindReturns);
|
package/cjs/schema/bulk.cjs
CHANGED
|
@@ -19,8 +19,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var bulk_exports = {};
|
|
20
20
|
__export(bulk_exports, {
|
|
21
21
|
BulkGet: () => BulkGet,
|
|
22
|
+
BulkGetBound: () => BulkGetBound,
|
|
22
23
|
BulkRemove: () => BulkRemove,
|
|
24
|
+
BulkRemoveBound: () => BulkRemoveBound,
|
|
23
25
|
BulkSave: () => BulkSave,
|
|
26
|
+
BulkSaveBound: () => BulkSaveBound,
|
|
24
27
|
BulkSaveResponseSchema: () => BulkSaveResponseSchema
|
|
25
28
|
});
|
|
26
29
|
module.exports = __toCommonJS(bulk_exports);
|
|
@@ -40,11 +43,22 @@ const BulkSave = import_zod.z.function().args(
|
|
|
40
43
|
_id: import_zod.z.string()
|
|
41
44
|
}).passthrough())
|
|
42
45
|
).returns(import_zod.z.promise(BulkSaveResponseSchema));
|
|
46
|
+
const BulkSaveBound = import_zod.z.function().args(
|
|
47
|
+
import_zod.z.array(import_zod.z.object({
|
|
48
|
+
_id: import_zod.z.string()
|
|
49
|
+
}).passthrough())
|
|
50
|
+
).returns(import_zod.z.promise(BulkSaveResponseSchema));
|
|
43
51
|
const BulkGet = import_zod.z.function().args(
|
|
44
52
|
import_config.CouchConfig,
|
|
45
53
|
import_zod.z.array(import_zod.z.string().describe("the ids to get"))
|
|
46
54
|
).returns(import_zod.z.promise(import_zod.z.array(import_crud.CouchDoc)));
|
|
55
|
+
const BulkGetBound = import_zod.z.function().args(
|
|
56
|
+
import_zod.z.array(import_zod.z.string().describe("the ids to get"))
|
|
57
|
+
).returns(import_zod.z.promise(import_zod.z.array(import_crud.CouchDoc)));
|
|
47
58
|
const BulkRemove = import_zod.z.function().args(
|
|
48
59
|
import_config.CouchConfig,
|
|
49
60
|
import_zod.z.array(import_zod.z.string().describe("the ids to delete"))
|
|
50
61
|
).returns(import_zod.z.promise(BulkSaveResponseSchema));
|
|
62
|
+
const BulkRemoveBound = import_zod.z.function().args(
|
|
63
|
+
import_zod.z.array(import_zod.z.string().describe("the ids to delete"))
|
|
64
|
+
).returns(import_zod.z.promise(BulkSaveResponseSchema));
|
package/cjs/schema/config.cjs
CHANGED
|
@@ -22,6 +22,25 @@ __export(config_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(config_exports);
|
|
24
24
|
var import_zod = require("zod");
|
|
25
|
+
const LoggerSchema = import_zod.z.object({
|
|
26
|
+
error: import_zod.z.function().args(import_zod.z.any()).returns(import_zod.z.void()).optional(),
|
|
27
|
+
warn: import_zod.z.function().args(import_zod.z.any()).returns(import_zod.z.void()).optional(),
|
|
28
|
+
info: import_zod.z.function().args(import_zod.z.any()).returns(import_zod.z.void()).optional(),
|
|
29
|
+
debug: import_zod.z.function().args(import_zod.z.any()).returns(import_zod.z.void()).optional()
|
|
30
|
+
}).or(import_zod.z.function().args(
|
|
31
|
+
import_zod.z.string(),
|
|
32
|
+
// level
|
|
33
|
+
import_zod.z.any()
|
|
34
|
+
// message/args
|
|
35
|
+
).returns(import_zod.z.void()));
|
|
25
36
|
const CouchConfig = import_zod.z.object({
|
|
26
|
-
|
|
37
|
+
throwOnGetNotFound: import_zod.z.boolean().optional().default(false).describe("if a get is 404 should we throw or return undefined"),
|
|
38
|
+
couch: import_zod.z.string().describe("the url of the couch db"),
|
|
39
|
+
bindWithRetry: import_zod.z.boolean().optional().default(true).describe("should we bind with retry"),
|
|
40
|
+
maxRetries: import_zod.z.number().optional().default(3).describe("maximum number of retry attempts"),
|
|
41
|
+
initialDelay: import_zod.z.number().optional().default(1e3).describe("initial retry delay in milliseconds"),
|
|
42
|
+
backoffFactor: import_zod.z.number().optional().default(2).describe("multiplier for exponential backoff"),
|
|
43
|
+
logger: LoggerSchema.optional().describe("logging interface supporting winston-like or simple function interface"),
|
|
44
|
+
_normalizedLogger: import_zod.z.any().optional()
|
|
45
|
+
// Internal property for caching normalized logger
|
|
27
46
|
}).passthrough().describe("The std config object");
|
package/cjs/schema/crud.cjs
CHANGED
|
@@ -21,7 +21,9 @@ __export(crud_exports, {
|
|
|
21
21
|
CouchDoc: () => CouchDoc,
|
|
22
22
|
CouchDocResponse: () => CouchDocResponse,
|
|
23
23
|
CouchGet: () => CouchGet,
|
|
24
|
-
|
|
24
|
+
CouchGetBound: () => CouchGetBound,
|
|
25
|
+
CouchPut: () => CouchPut,
|
|
26
|
+
CouchPutBound: () => CouchPutBound
|
|
25
27
|
});
|
|
26
28
|
module.exports = __toCommonJS(crud_exports);
|
|
27
29
|
var import_zod = require("zod");
|
|
@@ -41,7 +43,13 @@ const CouchPut = import_zod.z.function().args(
|
|
|
41
43
|
import_config.CouchConfig,
|
|
42
44
|
CouchDoc
|
|
43
45
|
).returns(import_zod.z.promise(CouchDocResponse));
|
|
46
|
+
const CouchPutBound = import_zod.z.function().args(
|
|
47
|
+
CouchDoc
|
|
48
|
+
).returns(import_zod.z.promise(CouchDocResponse));
|
|
44
49
|
const CouchGet = import_zod.z.function().args(
|
|
45
50
|
import_config.CouchConfig,
|
|
46
51
|
import_zod.z.string().describe("the couch doc id")
|
|
47
52
|
).returns(import_zod.z.promise(CouchDoc.nullable()));
|
|
53
|
+
const CouchGetBound = import_zod.z.function().args(
|
|
54
|
+
import_zod.z.string().describe("the couch doc id")
|
|
55
|
+
).returns(import_zod.z.promise(CouchDoc.nullable()));
|
package/cjs/schema/patch.cjs
CHANGED
|
@@ -19,20 +19,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var patch_exports = {};
|
|
20
20
|
__export(patch_exports, {
|
|
21
21
|
Patch: () => Patch,
|
|
22
|
-
|
|
22
|
+
PatchBound: () => PatchBound,
|
|
23
23
|
PatchProperties: () => PatchProperties
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(patch_exports);
|
|
26
26
|
var import_zod = require("zod");
|
|
27
27
|
var import_config = require("./config.cjs");
|
|
28
28
|
var import_crud = require("./crud.cjs");
|
|
29
|
-
const PatchConfig = import_config.CouchConfig.extend({
|
|
30
|
-
retries: import_zod.z.number().min(0).max(100).optional(),
|
|
31
|
-
delay: import_zod.z.number().min(0).optional()
|
|
32
|
-
});
|
|
33
29
|
const PatchProperties = import_zod.z.record(import_zod.z.string(), import_zod.z.any());
|
|
34
30
|
const Patch = import_zod.z.function().args(
|
|
35
|
-
|
|
31
|
+
import_config.CouchConfig,
|
|
32
|
+
import_zod.z.string().describe("the couch doc id"),
|
|
33
|
+
PatchProperties
|
|
34
|
+
).returns(import_zod.z.promise(import_crud.CouchDocResponse));
|
|
35
|
+
const PatchBound = import_zod.z.function().args(
|
|
36
36
|
import_zod.z.string().describe("the couch doc id"),
|
|
37
37
|
PatchProperties
|
|
38
38
|
).returns(import_zod.z.promise(import_crud.CouchDocResponse));
|
package/cjs/schema/query.cjs
CHANGED
|
@@ -18,34 +18,43 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var query_exports = {};
|
|
20
20
|
__export(query_exports, {
|
|
21
|
+
SimpleViewOptions: () => SimpleViewOptions,
|
|
21
22
|
SimpleViewQuery: () => SimpleViewQuery,
|
|
22
|
-
|
|
23
|
+
SimpleViewQueryBound: () => SimpleViewQueryBound,
|
|
24
|
+
SimpleViewQueryResponse: () => SimpleViewQueryResponse,
|
|
25
|
+
ViewRow: () => ViewRow
|
|
23
26
|
});
|
|
24
27
|
module.exports = __toCommonJS(query_exports);
|
|
25
28
|
var import_zod = require("zod");
|
|
26
29
|
var import_config = require("./config.cjs");
|
|
30
|
+
const ViewRow = import_zod.z.object({
|
|
31
|
+
id: import_zod.z.string().optional(),
|
|
32
|
+
key: import_zod.z.any().nullable(),
|
|
33
|
+
value: import_zod.z.any().nullable(),
|
|
34
|
+
doc: import_zod.z.object({}).passthrough().optional()
|
|
35
|
+
});
|
|
27
36
|
const SimpleViewQueryResponse = import_zod.z.object({
|
|
28
37
|
error: import_zod.z.string().optional().describe("if something is wrong"),
|
|
29
|
-
rows: import_zod.z.array(
|
|
30
|
-
id: import_zod.z.string().optional(),
|
|
31
|
-
key: import_zod.z.any().nullable(),
|
|
32
|
-
value: import_zod.z.any().nullable(),
|
|
33
|
-
doc: import_zod.z.object({}).passthrough().optional()
|
|
34
|
-
}))
|
|
38
|
+
rows: import_zod.z.array(ViewRow)
|
|
35
39
|
}).passthrough();
|
|
40
|
+
const SimpleViewOptions = import_zod.z.object({
|
|
41
|
+
startkey: import_zod.z.any().optional(),
|
|
42
|
+
endkey: import_zod.z.any().optional(),
|
|
43
|
+
descending: import_zod.z.boolean().optional().describe("sort results descending"),
|
|
44
|
+
skip: import_zod.z.number().positive().optional().describe("skip this many rows"),
|
|
45
|
+
limit: import_zod.z.number().positive().optional().describe("limit the results to this many rows"),
|
|
46
|
+
key: import_zod.z.any().optional(),
|
|
47
|
+
include_docs: import_zod.z.boolean().optional().describe("join the id to the doc and return it"),
|
|
48
|
+
reduce: import_zod.z.boolean().optional().describe("reduce the results"),
|
|
49
|
+
group: import_zod.z.boolean().optional().describe("group the results"),
|
|
50
|
+
group_level: import_zod.z.number().positive().optional().describe("group the results at this level")
|
|
51
|
+
}).optional().describe("query options");
|
|
36
52
|
const SimpleViewQuery = import_zod.z.function().args(
|
|
37
53
|
import_config.CouchConfig,
|
|
38
54
|
import_zod.z.string().describe("the view name"),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
limit: import_zod.z.number().positive().optional().describe("limit the results to this many rows"),
|
|
45
|
-
key: import_zod.z.any().optional(),
|
|
46
|
-
include_docs: import_zod.z.boolean().optional().describe("join the id to the doc and return it"),
|
|
47
|
-
reduce: import_zod.z.boolean().optional().describe("reduce the results"),
|
|
48
|
-
group: import_zod.z.boolean().optional().describe("group the results"),
|
|
49
|
-
group_level: import_zod.z.number().positive().optional().describe("group the results at this level")
|
|
50
|
-
}).optional().describe("query options")
|
|
55
|
+
SimpleViewOptions
|
|
56
|
+
).returns(import_zod.z.promise(SimpleViewQueryResponse));
|
|
57
|
+
const SimpleViewQueryBound = import_zod.z.function().args(
|
|
58
|
+
import_zod.z.string().describe("the view name"),
|
|
59
|
+
SimpleViewOptions
|
|
51
60
|
).returns(import_zod.z.promise(SimpleViewQueryResponse));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var stream_exports = {};
|
|
20
|
+
__export(stream_exports, {
|
|
21
|
+
OnRow: () => OnRow,
|
|
22
|
+
SimpleViewQueryStream: () => SimpleViewQueryStream,
|
|
23
|
+
SimpleViewQueryStreamBound: () => SimpleViewQueryStreamBound
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(stream_exports);
|
|
26
|
+
var import_zod = require("zod");
|
|
27
|
+
var import_config = require("./config.cjs");
|
|
28
|
+
var import_query = require("./query.cjs");
|
|
29
|
+
const OnRow = import_zod.z.function().args(
|
|
30
|
+
import_query.ViewRow
|
|
31
|
+
).returns(import_zod.z.undefined());
|
|
32
|
+
const SimpleViewQueryStream = import_zod.z.function().args(
|
|
33
|
+
import_config.CouchConfig,
|
|
34
|
+
import_zod.z.string().describe("the view name"),
|
|
35
|
+
import_query.SimpleViewOptions,
|
|
36
|
+
OnRow
|
|
37
|
+
).returns(import_zod.z.promise(import_zod.z.undefined()));
|
|
38
|
+
const SimpleViewQueryStreamBound = import_zod.z.function().args(
|
|
39
|
+
import_zod.z.string().describe("the view name"),
|
|
40
|
+
import_query.SimpleViewOptions,
|
|
41
|
+
OnRow
|
|
42
|
+
).returns(import_zod.z.promise(import_zod.z.undefined()));
|
package/impl/bulk.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import needle from 'needle'
|
|
3
3
|
import { BulkSave, BulkGet, BulkRemove } from '../schema/bulk.mjs'
|
|
4
|
+
import { RetryableError } from './errors.mjs'
|
|
5
|
+
import { createLogger } from './logger.mjs'
|
|
4
6
|
|
|
5
7
|
const opts = {
|
|
6
8
|
json: true,
|
|
@@ -11,24 +13,71 @@ const opts = {
|
|
|
11
13
|
|
|
12
14
|
/** @type { import('../schema/bulk.mjs').BulkSaveSchema } */
|
|
13
15
|
export const bulkSave = BulkSave.implement(async (config, docs) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
/** @type {import('./logger.mjs').Logger } */
|
|
17
|
+
const logger = createLogger(config)
|
|
18
|
+
|
|
19
|
+
if (!docs) {
|
|
20
|
+
logger.warn('bulkSave called with no docs')
|
|
21
|
+
return { ok: false, error: 'noDocs', reason: 'no docs provided' }
|
|
22
|
+
}
|
|
23
|
+
if (!docs.length) {
|
|
24
|
+
logger.warn('bulkSave called with empty docs array')
|
|
25
|
+
return { ok: false, error: 'noDocs', reason: 'no docs provided' }
|
|
26
|
+
}
|
|
16
27
|
|
|
28
|
+
logger.info(`Starting bulk save of ${docs.length} documents`)
|
|
17
29
|
const url = `${config.couch}/_bulk_docs`
|
|
18
30
|
const body = { docs }
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
let resp
|
|
32
|
+
try {
|
|
33
|
+
resp = await needle('post', url, body, opts)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.error('Network error during bulk save:', err)
|
|
36
|
+
RetryableError.handleNetworkError(err)
|
|
37
|
+
}
|
|
38
|
+
if (!resp) {
|
|
39
|
+
logger.error('No response received from bulk save request')
|
|
40
|
+
throw new RetryableError('no response', 503)
|
|
41
|
+
}
|
|
42
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
43
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
44
|
+
throw new RetryableError('retryable error during bulk save', resp.statusCode)
|
|
45
|
+
}
|
|
46
|
+
if (resp.statusCode !== 201) {
|
|
47
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
48
|
+
throw new Error('could not save')
|
|
49
|
+
}
|
|
21
50
|
const results = resp?.body || []
|
|
22
51
|
return results
|
|
23
52
|
})
|
|
24
53
|
|
|
25
54
|
/** @type { import('../schema/bulk.mjs').BulkGetSchema } */
|
|
26
55
|
export const bulkGet = BulkGet.implement(async (config, ids) => {
|
|
56
|
+
const logger = createLogger(config)
|
|
27
57
|
const keys = ids
|
|
58
|
+
|
|
59
|
+
logger.info(`Starting bulk get for ${keys.length} documents`)
|
|
28
60
|
const url = `${config.couch}/_all_docs?include_docs=true`
|
|
29
61
|
const body = { keys }
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
let resp
|
|
63
|
+
try {
|
|
64
|
+
resp = await needle('post', url, body, opts)
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logger.error('Network error during bulk get:', err)
|
|
67
|
+
RetryableError.handleNetworkError(err)
|
|
68
|
+
}
|
|
69
|
+
if (!resp) {
|
|
70
|
+
logger.error('No response received from bulk get request')
|
|
71
|
+
throw new RetryableError('no response', 503)
|
|
72
|
+
}
|
|
73
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
74
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
75
|
+
throw new RetryableError('retryable error during bulk get', resp.statusCode)
|
|
76
|
+
}
|
|
77
|
+
if (resp.statusCode !== 200) {
|
|
78
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
79
|
+
throw new Error('could not fetch')
|
|
80
|
+
}
|
|
32
81
|
const rows = resp?.body?.rows || []
|
|
33
82
|
/** @type {Array<import('../schema/crud.mjs').CouchDocSchema>} */
|
|
34
83
|
const docs = []
|
|
@@ -42,11 +91,14 @@ export const bulkGet = BulkGet.implement(async (config, ids) => {
|
|
|
42
91
|
const doc = r.doc
|
|
43
92
|
docs.push(doc)
|
|
44
93
|
})
|
|
94
|
+
logger.info(`Successfully retrieved ${docs.length} documents`)
|
|
45
95
|
return docs
|
|
46
96
|
})
|
|
47
97
|
|
|
48
98
|
/** @type { import('../schema/bulk.mjs').BulkRemoveSchema } */
|
|
49
99
|
export const bulkRemove = BulkRemove.implement(async (config, ids) => {
|
|
100
|
+
const logger = createLogger(config)
|
|
101
|
+
logger.info(`Starting bulk remove for ${ids.length} documents`)
|
|
50
102
|
const docs = await bulkGet(config, ids)
|
|
51
103
|
docs.forEach(d => { d._deleted = true })
|
|
52
104
|
return bulkSave(config, docs)
|
package/impl/crud.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import needle from 'needle'
|
|
3
3
|
import { CouchGet, CouchPut } from '../schema/crud.mjs'
|
|
4
|
+
import { RetryableError } from './errors.mjs'
|
|
5
|
+
import { createLogger } from './logger.mjs'
|
|
4
6
|
|
|
5
7
|
const opts = {
|
|
6
8
|
json: true,
|
|
@@ -9,24 +11,83 @@ const opts = {
|
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/** @type { import('../schema/crud.mjs').CouchGetSchema } */
|
|
12
15
|
export const get = CouchGet.implement(async (config, id) => {
|
|
16
|
+
const logger = createLogger(config)
|
|
13
17
|
const url = `${config.couch}/${id}`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
logger.info(`Getting document with id: ${id}`)
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const resp = await needle('get', url, opts)
|
|
22
|
+
if (!resp) {
|
|
23
|
+
logger.error('No response received from get request')
|
|
24
|
+
throw new RetryableError('no response', 503)
|
|
25
|
+
}
|
|
26
|
+
if (resp.statusCode === 404) {
|
|
27
|
+
logger.debug(`Document not found: ${id}`)
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
const result = resp?.body || {}
|
|
31
|
+
if (resp.statusCode === 404) {
|
|
32
|
+
if (config.throwOnGetNotFound) {
|
|
33
|
+
logger.warn(`Document not found (throwing error): ${id}`)
|
|
34
|
+
throw new Error(result.reason || 'not_found')
|
|
35
|
+
} else {
|
|
36
|
+
logger.debug(`Document not found (returning undefined): ${id}`)
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
41
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
42
|
+
throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
|
|
43
|
+
}
|
|
44
|
+
if (resp.statusCode !== 200) {
|
|
45
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`)
|
|
46
|
+
throw new Error(result.reason || 'failed')
|
|
47
|
+
}
|
|
48
|
+
logger.info(`Successfully retrieved document: ${id}`)
|
|
49
|
+
return result
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error('Error during get operation:', err)
|
|
52
|
+
RetryableError.handleNetworkError(err)
|
|
53
|
+
}
|
|
19
54
|
})
|
|
20
55
|
|
|
56
|
+
/** @type { import('../schema/crud.mjs').CouchPutSchema } */
|
|
21
57
|
export const put = CouchPut.implement(async (config, doc) => {
|
|
58
|
+
const logger = createLogger(config)
|
|
22
59
|
const url = `${config.couch}/${doc._id}`
|
|
23
60
|
const body = doc
|
|
24
|
-
|
|
61
|
+
|
|
62
|
+
logger.info(`Putting document with id: ${doc._id}`)
|
|
63
|
+
let resp
|
|
64
|
+
try {
|
|
65
|
+
resp = await needle('put', url, body, opts)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error('Error during put operation:', err)
|
|
68
|
+
RetryableError.handleNetworkError(err)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!resp) {
|
|
72
|
+
logger.error('No response received from put request')
|
|
73
|
+
throw new RetryableError('no response', 503)
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
const result = resp?.body || {}
|
|
26
77
|
result.statusCode = resp.statusCode
|
|
78
|
+
|
|
27
79
|
if (resp.statusCode === 409) {
|
|
80
|
+
logger.warn(`Conflict detected for document: ${doc._id}`)
|
|
28
81
|
result.ok = false
|
|
29
82
|
result.error = 'conflict'
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
87
|
+
logger.warn(`Retryable status code received: ${resp.statusCode}`)
|
|
88
|
+
throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
|
|
30
89
|
}
|
|
90
|
+
|
|
91
|
+
logger.info(`Successfully saved document: ${doc._id}`)
|
|
31
92
|
return result
|
|
32
93
|
})
|
package/impl/errors.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} NetworkError
|
|
5
|
+
* @property {string} code - The error code
|
|
6
|
+
* @property {string} [message] - Optional error message
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class RetryableError extends Error {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} message - The error message
|
|
12
|
+
* @param {number|undefined} statusCode - The HTTP status code
|
|
13
|
+
*/
|
|
14
|
+
constructor(message, statusCode) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'RetryableError';
|
|
17
|
+
this.statusCode = statusCode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {number|undefined} statusCode - The HTTP status code to check
|
|
22
|
+
* @returns {boolean} Whether the status code is retryable
|
|
23
|
+
*/
|
|
24
|
+
static isRetryableStatusCode(statusCode) {
|
|
25
|
+
if (statusCode === undefined) return false;
|
|
26
|
+
return [408, 429, 500, 502, 503, 504].includes(statusCode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {NetworkError | unknown} err - The network error to handle
|
|
31
|
+
* @throws {RetryableError} If the error is retryable
|
|
32
|
+
* @throws {Error} If the error is not retryable
|
|
33
|
+
*/
|
|
34
|
+
static handleNetworkError(err) {
|
|
35
|
+
/** @type {Record<string, number>} */
|
|
36
|
+
const networkErrors = {
|
|
37
|
+
ECONNREFUSED: 503,
|
|
38
|
+
ECONNRESET: 503,
|
|
39
|
+
ETIMEDOUT: 503,
|
|
40
|
+
ENETUNREACH: 503,
|
|
41
|
+
ENOTFOUND: 503,
|
|
42
|
+
EPIPE: 503,
|
|
43
|
+
EHOSTUNREACH: 503,
|
|
44
|
+
ESOCKETTIMEDOUT: 503
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Type guard for NetworkError shape
|
|
48
|
+
if (typeof err === 'object' && err !== null && 'code' in err && typeof err.code === 'string' && networkErrors[err.code]) {
|
|
49
|
+
throw new RetryableError(`Network error: ${err.code}`, networkErrors[err.code]);
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/impl/logger.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} Logger
|
|
3
|
+
* @property {(...args: any[]) => void} error - Log error messages
|
|
4
|
+
* @property {(...args: any[]) => void} warn - Log warning messages
|
|
5
|
+
* @property {(...args: any[]) => void} info - Log info messages
|
|
6
|
+
* @property {(...args: any[]) => void} debug - Log debug messages
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a unified logger interface that works with both function and object-style loggers
|
|
11
|
+
* @param {import('../schema/config.mjs').CouchConfigSchema} config
|
|
12
|
+
* @returns {Logger} Normalized logger interface
|
|
13
|
+
*/
|
|
14
|
+
export function createLogger(config) {
|
|
15
|
+
// Return cached logger if it exists
|
|
16
|
+
if (config._normalizedLogger) {
|
|
17
|
+
return config._normalizedLogger
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If no logger provided, use console if useConsoleLogger is set, otherwise return no-op logger
|
|
21
|
+
if (!config.logger) {
|
|
22
|
+
if (config.useConsoleLogger) {
|
|
23
|
+
config._normalizedLogger = {
|
|
24
|
+
error: (...args) => console.error(...args),
|
|
25
|
+
warn: (...args) => console.warn(...args),
|
|
26
|
+
info: (...args) => console.info(...args),
|
|
27
|
+
debug: (...args) => console.debug(...args)
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
config._normalizedLogger = {
|
|
31
|
+
error: () => {},
|
|
32
|
+
warn: () => {},
|
|
33
|
+
info: () => {},
|
|
34
|
+
debug: () => {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return config._normalizedLogger
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// If logger is a function, wrap it to provide object interface
|
|
41
|
+
if (typeof config.logger === 'function') {
|
|
42
|
+
config._normalizedLogger = {
|
|
43
|
+
error: (...args) => config.logger('error', ...args),
|
|
44
|
+
warn: (...args) => config.logger('warn', ...args),
|
|
45
|
+
info: (...args) => config.logger('info', ...args),
|
|
46
|
+
debug: (...args) => config.logger('debug', ...args)
|
|
47
|
+
}
|
|
48
|
+
return config._normalizedLogger
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If logger is an object, use its methods or provide no-ops for missing ones
|
|
52
|
+
config._normalizedLogger = {
|
|
53
|
+
error: config.logger.error || (() => {}),
|
|
54
|
+
warn: config.logger.warn || (() => {}),
|
|
55
|
+
info: config.logger.info || (() => {}),
|
|
56
|
+
debug: config.logger.debug || (() => {})
|
|
57
|
+
}
|
|
58
|
+
return config._normalizedLogger
|
|
59
|
+
}
|
package/impl/patch.mjs
CHANGED
|
@@ -1,36 +1,68 @@
|
|
|
1
1
|
import { get, put } from './crud.mjs'
|
|
2
2
|
import { Patch } from '../schema/patch.mjs'
|
|
3
|
+
import { createLogger } from './logger.mjs'
|
|
3
4
|
|
|
4
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
5
|
+
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
5
6
|
|
|
7
|
+
/** @type { import('../schema/patch.mjs').PatchSchema } */
|
|
6
8
|
export const patch = Patch.implement(async (config, id, properties) => {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
+
const logger = createLogger(config)
|
|
10
|
+
const maxRetries = config.maxRetries || 5
|
|
11
|
+
let delay = config.initialDelay || 1000
|
|
9
12
|
let attempts = 0
|
|
10
13
|
|
|
14
|
+
logger.info(`Starting patch operation for document ${id}`)
|
|
15
|
+
logger.debug('Patch properties:', properties)
|
|
16
|
+
|
|
11
17
|
while (attempts <= maxRetries) {
|
|
18
|
+
logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`)
|
|
12
19
|
try {
|
|
13
20
|
const doc = await get(config, id)
|
|
21
|
+
if (!doc) {
|
|
22
|
+
logger.warn(`Document ${id} not found`)
|
|
23
|
+
return { ok: false, statusCode: 404, error: 'not_found' }
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
const updatedDoc = { ...doc, ...properties }
|
|
27
|
+
logger.debug('Merged document:', updatedDoc)
|
|
28
|
+
|
|
15
29
|
const result = await put(config, updatedDoc)
|
|
16
30
|
|
|
17
31
|
// Check if the response indicates a conflict
|
|
18
32
|
if (result.ok) {
|
|
33
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`)
|
|
19
34
|
return result
|
|
20
35
|
}
|
|
36
|
+
|
|
21
37
|
// If not ok, treat as conflict and retry
|
|
22
38
|
attempts++
|
|
23
39
|
if (attempts > maxRetries) {
|
|
40
|
+
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`)
|
|
24
41
|
throw new Error(`Failed to patch after ${maxRetries} attempts`)
|
|
25
42
|
}
|
|
43
|
+
|
|
44
|
+
logger.warn(`Conflict detected for ${id}, retrying (attempt ${attempts})`)
|
|
26
45
|
await sleep(delay)
|
|
46
|
+
delay *= config.backoffFactor
|
|
47
|
+
logger.debug(`Next retry delay: ${delay}ms`)
|
|
48
|
+
|
|
27
49
|
} catch (err) {
|
|
50
|
+
if (err.message === 'not_found') {
|
|
51
|
+
logger.warn(`Document ${id} not found during patch operation`)
|
|
52
|
+
return { ok: false, statusCode: 404, error: 'not_found' }
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
// Handle other errors (network, etc)
|
|
29
56
|
attempts++
|
|
30
57
|
if (attempts > maxRetries) {
|
|
31
|
-
|
|
58
|
+
const error = `Failed to patch after ${maxRetries} attempts: ${err.message}`
|
|
59
|
+
logger.error(error)
|
|
60
|
+
return { ok: false, statusCode: 500, error }
|
|
32
61
|
}
|
|
62
|
+
|
|
63
|
+
logger.warn(`Error during patch attempt ${attempts}: ${err.message}`)
|
|
33
64
|
await sleep(delay)
|
|
65
|
+
logger.debug(`Retrying after ${delay}ms`)
|
|
34
66
|
}
|
|
35
67
|
}
|
|
36
68
|
})
|