@ekodb/ekodb-client 0.23.1 → 0.25.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/dist/client.d.ts +62 -1
- package/dist/client.js +92 -5
- package/dist/client.test.js +87 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/package.json +2 -2
- package/src/client.test.ts +102 -0
- package/src/client.ts +111 -4
- package/src/index.ts +6 -0
package/dist/client.d.ts
CHANGED
|
@@ -94,6 +94,55 @@ export interface UpsertOptions {
|
|
|
94
94
|
transactionId?: string;
|
|
95
95
|
bypassCache?: boolean;
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* A health status value. Like the Go client's `HealthState` (a string type), an
|
|
99
|
+
* off-contract status reported by the server is preserved verbatim.
|
|
100
|
+
*/
|
|
101
|
+
export type HealthState = string;
|
|
102
|
+
/** Canonical {@link HealthStatus.status} values. */
|
|
103
|
+
export declare const HealthOK = "ok";
|
|
104
|
+
export declare const HealthDegraded = "degraded";
|
|
105
|
+
export declare const HealthUnknown = "unknown";
|
|
106
|
+
/**
|
|
107
|
+
* A snapshot of an ekoDB /api/health probe.
|
|
108
|
+
*
|
|
109
|
+
* It is degraded-tolerant: a reachable server that reports `degraded` is a
|
|
110
|
+
* successful snapshot (`reachable: true`, `status: "degraded"`), NOT an error.
|
|
111
|
+
* An unreachable/unparseable probe yields `{ reachable: false, status: "unknown" }`.
|
|
112
|
+
*
|
|
113
|
+
* Consumers base liveness on `reachable` and treat `degraded` as a warning,
|
|
114
|
+
* never as a fatal. `detail` (the full admin body, which includes internal
|
|
115
|
+
* metrics and collection names) is excluded from JSON via `toJSON()` so
|
|
116
|
+
* surfacing the snapshot cannot leak internals; read it in-process when needed.
|
|
117
|
+
*/
|
|
118
|
+
export declare class HealthStatus {
|
|
119
|
+
reachable: boolean;
|
|
120
|
+
status: HealthState;
|
|
121
|
+
integrityOk: boolean;
|
|
122
|
+
detail?: {
|
|
123
|
+
[key: string]: any;
|
|
124
|
+
};
|
|
125
|
+
constructor(init: {
|
|
126
|
+
reachable: boolean;
|
|
127
|
+
status: HealthState;
|
|
128
|
+
integrityOk: boolean;
|
|
129
|
+
detail?: {
|
|
130
|
+
[key: string]: any;
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
toJSON(): {
|
|
134
|
+
reachable: boolean;
|
|
135
|
+
status: HealthState;
|
|
136
|
+
integrity_ok: boolean;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Interprets a parsed /api/health body per the shared health contract. A
|
|
141
|
+
* missing/odd `status` on a reachable body fails safe to `degraded`; a
|
|
142
|
+
* non-object body yields an unreachable `unknown` snapshot. `integrity_ok` is
|
|
143
|
+
* read from the top-level field (public) or nested `integrity.healthy` (admin).
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseHealthStatus(body: any): HealthStatus;
|
|
97
146
|
export interface FindOptions {
|
|
98
147
|
filter?: any;
|
|
99
148
|
sort?: any;
|
|
@@ -861,9 +910,21 @@ export declare class EkoDBClient {
|
|
|
861
910
|
*/
|
|
862
911
|
distinctValues(collection: string, field: string, options?: DistinctValuesOptions): Promise<DistinctValuesResponse>;
|
|
863
912
|
/**
|
|
864
|
-
* Health check -
|
|
913
|
+
* Health check - reports whether the ekoDB server is reachable.
|
|
914
|
+
*
|
|
915
|
+
* Returns `true` whenever the server responds (INCLUDING when it reports
|
|
916
|
+
* `degraded`) and `false` only when it is unreachable. The ekoDB server
|
|
917
|
+
* returns HTTP 200 while degraded on purpose, so gating on `status === "ok"`
|
|
918
|
+
* would treat a degraded-but-serving server as down. Use {@link healthStatus}
|
|
919
|
+
* for the ok/degraded distinction.
|
|
865
920
|
*/
|
|
866
921
|
health(): Promise<boolean>;
|
|
922
|
+
/**
|
|
923
|
+
* Structured, degraded-tolerant health. Returns a {@link HealthStatus}
|
|
924
|
+
* snapshot; an unreachable server yields `{ reachable: false, status:
|
|
925
|
+
* "unknown" }` rather than throwing.
|
|
926
|
+
*/
|
|
927
|
+
healthStatus(): Promise<HealthStatus>;
|
|
867
928
|
/**
|
|
868
929
|
* Execute a tool via ekoDB's server-side tool pipeline.
|
|
869
930
|
*
|
package/dist/client.js
CHANGED
|
@@ -36,7 +36,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
|
|
39
|
+
exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.HealthStatus = exports.HealthUnknown = exports.HealthDegraded = exports.HealthOK = exports.RateLimitError = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.SerializationFormat = void 0;
|
|
40
|
+
exports.parseHealthStatus = parseHealthStatus;
|
|
40
41
|
exports.extractRecordId = extractRecordId;
|
|
41
42
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
42
43
|
const query_builder_1 = require("./query-builder");
|
|
@@ -71,6 +72,74 @@ class RateLimitError extends Error {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
exports.RateLimitError = RateLimitError;
|
|
75
|
+
/** Canonical {@link HealthStatus.status} values. */
|
|
76
|
+
exports.HealthOK = "ok";
|
|
77
|
+
exports.HealthDegraded = "degraded";
|
|
78
|
+
exports.HealthUnknown = "unknown";
|
|
79
|
+
/**
|
|
80
|
+
* A snapshot of an ekoDB /api/health probe.
|
|
81
|
+
*
|
|
82
|
+
* It is degraded-tolerant: a reachable server that reports `degraded` is a
|
|
83
|
+
* successful snapshot (`reachable: true`, `status: "degraded"`), NOT an error.
|
|
84
|
+
* An unreachable/unparseable probe yields `{ reachable: false, status: "unknown" }`.
|
|
85
|
+
*
|
|
86
|
+
* Consumers base liveness on `reachable` and treat `degraded` as a warning,
|
|
87
|
+
* never as a fatal. `detail` (the full admin body, which includes internal
|
|
88
|
+
* metrics and collection names) is excluded from JSON via `toJSON()` so
|
|
89
|
+
* surfacing the snapshot cannot leak internals; read it in-process when needed.
|
|
90
|
+
*/
|
|
91
|
+
class HealthStatus {
|
|
92
|
+
constructor(init) {
|
|
93
|
+
this.reachable = init.reachable;
|
|
94
|
+
this.status = init.status;
|
|
95
|
+
this.integrityOk = init.integrityOk;
|
|
96
|
+
this.detail = init.detail;
|
|
97
|
+
}
|
|
98
|
+
toJSON() {
|
|
99
|
+
return {
|
|
100
|
+
reachable: this.reachable,
|
|
101
|
+
status: this.status,
|
|
102
|
+
integrity_ok: this.integrityOk,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.HealthStatus = HealthStatus;
|
|
107
|
+
/**
|
|
108
|
+
* Interprets a parsed /api/health body per the shared health contract. A
|
|
109
|
+
* missing/odd `status` on a reachable body fails safe to `degraded`; a
|
|
110
|
+
* non-object body yields an unreachable `unknown` snapshot. `integrity_ok` is
|
|
111
|
+
* read from the top-level field (public) or nested `integrity.healthy` (admin).
|
|
112
|
+
*/
|
|
113
|
+
function parseHealthStatus(body) {
|
|
114
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
115
|
+
return new HealthStatus({
|
|
116
|
+
reachable: false,
|
|
117
|
+
status: exports.HealthUnknown,
|
|
118
|
+
integrityOk: false,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Default to degraded; a non-empty string status is kept verbatim (matches
|
|
122
|
+
// the Go client). A missing/non-string status fails safe to degraded.
|
|
123
|
+
let status = exports.HealthDegraded;
|
|
124
|
+
if (typeof body.status === "string" && body.status !== "") {
|
|
125
|
+
status = body.status;
|
|
126
|
+
}
|
|
127
|
+
let integrityOk = false;
|
|
128
|
+
if (typeof body.integrity_ok === "boolean") {
|
|
129
|
+
integrityOk = body.integrity_ok;
|
|
130
|
+
}
|
|
131
|
+
else if (body.integrity &&
|
|
132
|
+
typeof body.integrity === "object" &&
|
|
133
|
+
typeof body.integrity.healthy === "boolean") {
|
|
134
|
+
integrityOk = body.integrity.healthy;
|
|
135
|
+
}
|
|
136
|
+
return new HealthStatus({
|
|
137
|
+
reachable: true,
|
|
138
|
+
status,
|
|
139
|
+
integrityOk,
|
|
140
|
+
detail: body,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
74
143
|
var MergeStrategy;
|
|
75
144
|
(function (MergeStrategy) {
|
|
76
145
|
MergeStrategy["Chronological"] = "Chronological";
|
|
@@ -1121,15 +1190,33 @@ class EkoDBClient {
|
|
|
1121
1190
|
return this.makeRequest("POST", `/api/distinct/${encodeURIComponent(collection)}/${encodeURIComponent(field)}`, body, 0, true);
|
|
1122
1191
|
}
|
|
1123
1192
|
/**
|
|
1124
|
-
* Health check -
|
|
1193
|
+
* Health check - reports whether the ekoDB server is reachable.
|
|
1194
|
+
*
|
|
1195
|
+
* Returns `true` whenever the server responds (INCLUDING when it reports
|
|
1196
|
+
* `degraded`) and `false` only when it is unreachable. The ekoDB server
|
|
1197
|
+
* returns HTTP 200 while degraded on purpose, so gating on `status === "ok"`
|
|
1198
|
+
* would treat a degraded-but-serving server as down. Use {@link healthStatus}
|
|
1199
|
+
* for the ok/degraded distinction.
|
|
1125
1200
|
*/
|
|
1126
1201
|
async health() {
|
|
1202
|
+
return (await this.healthStatus()).reachable;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Structured, degraded-tolerant health. Returns a {@link HealthStatus}
|
|
1206
|
+
* snapshot; an unreachable server yields `{ reachable: false, status:
|
|
1207
|
+
* "unknown" }` rather than throwing.
|
|
1208
|
+
*/
|
|
1209
|
+
async healthStatus() {
|
|
1127
1210
|
try {
|
|
1128
|
-
const
|
|
1129
|
-
return
|
|
1211
|
+
const body = await this.makeRequest("GET", "/api/health", undefined, 0, true);
|
|
1212
|
+
return parseHealthStatus(body);
|
|
1130
1213
|
}
|
|
1131
1214
|
catch {
|
|
1132
|
-
return
|
|
1215
|
+
return new HealthStatus({
|
|
1216
|
+
reachable: false,
|
|
1217
|
+
status: exports.HealthUnknown,
|
|
1218
|
+
integrityOk: false,
|
|
1219
|
+
});
|
|
1133
1220
|
}
|
|
1134
1221
|
}
|
|
1135
1222
|
// ========== Chat Methods ==========
|
package/dist/client.test.js
CHANGED
|
@@ -2671,3 +2671,90 @@ function mockErrorResponse(status, message) {
|
|
|
2671
2671
|
await (0, vitest_1.expect)(client.find("users")).rejects.toThrow(/timed out after 20ms/);
|
|
2672
2672
|
});
|
|
2673
2673
|
});
|
|
2674
|
+
// ============================================================================
|
|
2675
|
+
// Health Contract Tests
|
|
2676
|
+
// ============================================================================
|
|
2677
|
+
(0, vitest_1.describe)("health / healthStatus", () => {
|
|
2678
|
+
(0, vitest_1.it)("health() returns true for a reachable degraded server (bug fix)", async () => {
|
|
2679
|
+
const client = createTestClient();
|
|
2680
|
+
mockTokenResponse();
|
|
2681
|
+
mockJsonResponse({ status: "degraded", integrity_ok: false });
|
|
2682
|
+
(0, vitest_1.expect)(await client.health()).toBe(true);
|
|
2683
|
+
});
|
|
2684
|
+
(0, vitest_1.it)("health() returns false when unreachable", async () => {
|
|
2685
|
+
const client = createTestClient();
|
|
2686
|
+
mockTokenResponse();
|
|
2687
|
+
mockErrorResponse(503, "unavailable");
|
|
2688
|
+
(0, vitest_1.expect)(await client.health()).toBe(false);
|
|
2689
|
+
});
|
|
2690
|
+
(0, vitest_1.it)("healthStatus() surfaces degraded without erroring", async () => {
|
|
2691
|
+
const client = createTestClient();
|
|
2692
|
+
mockTokenResponse();
|
|
2693
|
+
mockJsonResponse({ status: "degraded", integrity_ok: false });
|
|
2694
|
+
const hs = await client.healthStatus();
|
|
2695
|
+
(0, vitest_1.expect)(hs.reachable).toBe(true);
|
|
2696
|
+
(0, vitest_1.expect)(hs.status).toBe("degraded");
|
|
2697
|
+
(0, vitest_1.expect)(hs.integrityOk).toBe(false);
|
|
2698
|
+
});
|
|
2699
|
+
(0, vitest_1.it)("healthStatus() unreachable -> reachable=false, status=unknown", async () => {
|
|
2700
|
+
const client = createTestClient();
|
|
2701
|
+
mockTokenResponse();
|
|
2702
|
+
mockErrorResponse(503, "unavailable");
|
|
2703
|
+
const hs = await client.healthStatus();
|
|
2704
|
+
(0, vitest_1.expect)(hs.reachable).toBe(false);
|
|
2705
|
+
(0, vitest_1.expect)(hs.status).toBe("unknown");
|
|
2706
|
+
});
|
|
2707
|
+
(0, vitest_1.it)("healthStatus() reads integrity from the admin nested shape", async () => {
|
|
2708
|
+
const client = createTestClient();
|
|
2709
|
+
mockTokenResponse();
|
|
2710
|
+
mockJsonResponse({
|
|
2711
|
+
status: "ok",
|
|
2712
|
+
integrity: { healthy: true, manifest_load_failed: [] },
|
|
2713
|
+
});
|
|
2714
|
+
const hs = await client.healthStatus();
|
|
2715
|
+
(0, vitest_1.expect)(hs.status).toBe("ok");
|
|
2716
|
+
(0, vitest_1.expect)(hs.integrityOk).toBe(true);
|
|
2717
|
+
});
|
|
2718
|
+
(0, vitest_1.it)("parseHealthStatus fails safe to degraded on a missing status", () => {
|
|
2719
|
+
const hs = (0, client_1.parseHealthStatus)({ integrity_ok: true });
|
|
2720
|
+
(0, vitest_1.expect)(hs.reachable).toBe(true);
|
|
2721
|
+
(0, vitest_1.expect)(hs.status).toBe("degraded");
|
|
2722
|
+
});
|
|
2723
|
+
(0, vitest_1.it)("parseHealthStatus yields an unknown snapshot for a non-object body", () => {
|
|
2724
|
+
const hs = (0, client_1.parseHealthStatus)("not json");
|
|
2725
|
+
(0, vitest_1.expect)(hs.reachable).toBe(false);
|
|
2726
|
+
(0, vitest_1.expect)(hs.status).toBe("unknown");
|
|
2727
|
+
});
|
|
2728
|
+
(0, vitest_1.it)("JSON serialization is a safe summary that excludes detail", () => {
|
|
2729
|
+
const hs = (0, client_1.parseHealthStatus)({
|
|
2730
|
+
status: "degraded",
|
|
2731
|
+
integrity: {
|
|
2732
|
+
healthy: false,
|
|
2733
|
+
manifest_load_failed: ["secret_collection"],
|
|
2734
|
+
},
|
|
2735
|
+
});
|
|
2736
|
+
const json = JSON.stringify(hs);
|
|
2737
|
+
(0, vitest_1.expect)(json).not.toContain("detail");
|
|
2738
|
+
(0, vitest_1.expect)(json).not.toContain("secret_collection");
|
|
2739
|
+
const parsed = JSON.parse(json);
|
|
2740
|
+
(0, vitest_1.expect)(parsed).toEqual({
|
|
2741
|
+
reachable: true,
|
|
2742
|
+
status: "degraded",
|
|
2743
|
+
integrity_ok: false,
|
|
2744
|
+
});
|
|
2745
|
+
// detail is still readable in-process
|
|
2746
|
+
(0, vitest_1.expect)(hs.detail).toBeDefined();
|
|
2747
|
+
});
|
|
2748
|
+
(0, vitest_1.it)("parseHealthStatus preserves a raw off-contract status (matches Go)", () => {
|
|
2749
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)({ status: "healthy" }).status).toBe("healthy");
|
|
2750
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)({ status: "ok" }).status).toBe("ok");
|
|
2751
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)({ status: "degraded" }).status).toBe("degraded");
|
|
2752
|
+
});
|
|
2753
|
+
(0, vitest_1.it)("parseHealthStatus fails safe to degraded on a non-string status", () => {
|
|
2754
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)({ status: { nested: 1 } }).status).toBe("degraded");
|
|
2755
|
+
});
|
|
2756
|
+
(0, vitest_1.it)("parseHealthStatus treats a non-object (array) body as unknown", () => {
|
|
2757
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)([1, 2, 3]).reachable).toBe(false);
|
|
2758
|
+
(0, vitest_1.expect)((0, client_1.parseHealthStatus)([1, 2, 3]).status).toBe("unknown");
|
|
2759
|
+
});
|
|
2760
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { EkoDBClient, WebSocketClient, EventStream, SerializationFormat, MergeStrategy, RateLimitError, SchemaCache, extractRecordId, } from "./client";
|
|
1
|
+
export { EkoDBClient, WebSocketClient, EventStream, SerializationFormat, MergeStrategy, RateLimitError, SchemaCache, extractRecordId, HealthStatus, parseHealthStatus, HealthOK, HealthDegraded, HealthUnknown, } from "./client";
|
|
2
|
+
export type { HealthState } from "./client";
|
|
2
3
|
export { QueryBuilder, SortOrder } from "./query-builder";
|
|
3
4
|
export { SearchQueryBuilder } from "./search";
|
|
4
5
|
export { SchemaBuilder, FieldTypeSchemaBuilder, VectorIndexAlgorithm, DistanceMetric, } from "./schema";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Field = exports.getObjectValue = exports.getVectorValue = exports.getSetValue = exports.getArrayValue = exports.getBinaryValue = exports.getBytesValue = exports.getDurationValue = exports.getDecimalValue = exports.getUUIDValue = exports.getDateTimeValue = exports.extractRecord = exports.getValues = exports.getValue = exports.parameterRef = exports.ChatMessage = exports.Stage = exports.JoinBuilder = exports.DistanceMetric = exports.VectorIndexAlgorithm = exports.FieldTypeSchemaBuilder = exports.SchemaBuilder = exports.SearchQueryBuilder = exports.SortOrder = exports.QueryBuilder = exports.extractRecordId = exports.SchemaCache = exports.RateLimitError = exports.MergeStrategy = exports.SerializationFormat = exports.EventStream = exports.WebSocketClient = exports.EkoDBClient = void 0;
|
|
3
|
+
exports.Field = exports.getObjectValue = exports.getVectorValue = exports.getSetValue = exports.getArrayValue = exports.getBinaryValue = exports.getBytesValue = exports.getDurationValue = exports.getDecimalValue = exports.getUUIDValue = exports.getDateTimeValue = exports.extractRecord = exports.getValues = exports.getValue = exports.parameterRef = exports.ChatMessage = exports.Stage = exports.JoinBuilder = exports.DistanceMetric = exports.VectorIndexAlgorithm = exports.FieldTypeSchemaBuilder = exports.SchemaBuilder = exports.SearchQueryBuilder = exports.SortOrder = exports.QueryBuilder = exports.HealthUnknown = exports.HealthDegraded = exports.HealthOK = exports.parseHealthStatus = exports.HealthStatus = exports.extractRecordId = exports.SchemaCache = exports.RateLimitError = exports.MergeStrategy = exports.SerializationFormat = exports.EventStream = exports.WebSocketClient = exports.EkoDBClient = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "EkoDBClient", { enumerable: true, get: function () { return client_1.EkoDBClient; } });
|
|
6
6
|
Object.defineProperty(exports, "WebSocketClient", { enumerable: true, get: function () { return client_1.WebSocketClient; } });
|
|
@@ -10,6 +10,11 @@ Object.defineProperty(exports, "MergeStrategy", { enumerable: true, get: functio
|
|
|
10
10
|
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return client_1.RateLimitError; } });
|
|
11
11
|
Object.defineProperty(exports, "SchemaCache", { enumerable: true, get: function () { return client_1.SchemaCache; } });
|
|
12
12
|
Object.defineProperty(exports, "extractRecordId", { enumerable: true, get: function () { return client_1.extractRecordId; } });
|
|
13
|
+
Object.defineProperty(exports, "HealthStatus", { enumerable: true, get: function () { return client_1.HealthStatus; } });
|
|
14
|
+
Object.defineProperty(exports, "parseHealthStatus", { enumerable: true, get: function () { return client_1.parseHealthStatus; } });
|
|
15
|
+
Object.defineProperty(exports, "HealthOK", { enumerable: true, get: function () { return client_1.HealthOK; } });
|
|
16
|
+
Object.defineProperty(exports, "HealthDegraded", { enumerable: true, get: function () { return client_1.HealthDegraded; } });
|
|
17
|
+
Object.defineProperty(exports, "HealthUnknown", { enumerable: true, get: function () { return client_1.HealthUnknown; } });
|
|
13
18
|
var query_builder_1 = require("./query-builder");
|
|
14
19
|
Object.defineProperty(exports, "QueryBuilder", { enumerable: true, get: function () { return query_builder_1.QueryBuilder; } });
|
|
15
20
|
Object.defineProperty(exports, "SortOrder", { enumerable: true, get: function () { return query_builder_1.SortOrder; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekodb/ekodb-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Official TypeScript/JavaScript client for ekoDB",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"author": "ekoDB",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/node": "^
|
|
23
|
+
"@types/node": "^24.13.2",
|
|
24
24
|
"@types/ws": "^8.18.1",
|
|
25
25
|
"typescript": "^5.9.3",
|
|
26
26
|
"vitest": "^4.0.18"
|
package/src/client.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SerializationFormat,
|
|
12
12
|
extractRecordId,
|
|
13
13
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
14
|
+
parseHealthStatus,
|
|
14
15
|
} from "./client";
|
|
15
16
|
import { SearchQueryBuilder } from "./search";
|
|
16
17
|
|
|
@@ -3518,3 +3519,104 @@ describe("request timeout", () => {
|
|
|
3518
3519
|
await expect(client.find("users")).rejects.toThrow(/timed out after 20ms/);
|
|
3519
3520
|
});
|
|
3520
3521
|
});
|
|
3522
|
+
|
|
3523
|
+
// ============================================================================
|
|
3524
|
+
// Health Contract Tests
|
|
3525
|
+
// ============================================================================
|
|
3526
|
+
|
|
3527
|
+
describe("health / healthStatus", () => {
|
|
3528
|
+
it("health() returns true for a reachable degraded server (bug fix)", async () => {
|
|
3529
|
+
const client = createTestClient();
|
|
3530
|
+
mockTokenResponse();
|
|
3531
|
+
mockJsonResponse({ status: "degraded", integrity_ok: false });
|
|
3532
|
+
expect(await client.health()).toBe(true);
|
|
3533
|
+
});
|
|
3534
|
+
|
|
3535
|
+
it("health() returns false when unreachable", async () => {
|
|
3536
|
+
const client = createTestClient();
|
|
3537
|
+
mockTokenResponse();
|
|
3538
|
+
mockErrorResponse(503, "unavailable");
|
|
3539
|
+
expect(await client.health()).toBe(false);
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
it("healthStatus() surfaces degraded without erroring", async () => {
|
|
3543
|
+
const client = createTestClient();
|
|
3544
|
+
mockTokenResponse();
|
|
3545
|
+
mockJsonResponse({ status: "degraded", integrity_ok: false });
|
|
3546
|
+
const hs = await client.healthStatus();
|
|
3547
|
+
expect(hs.reachable).toBe(true);
|
|
3548
|
+
expect(hs.status).toBe("degraded");
|
|
3549
|
+
expect(hs.integrityOk).toBe(false);
|
|
3550
|
+
});
|
|
3551
|
+
|
|
3552
|
+
it("healthStatus() unreachable -> reachable=false, status=unknown", async () => {
|
|
3553
|
+
const client = createTestClient();
|
|
3554
|
+
mockTokenResponse();
|
|
3555
|
+
mockErrorResponse(503, "unavailable");
|
|
3556
|
+
const hs = await client.healthStatus();
|
|
3557
|
+
expect(hs.reachable).toBe(false);
|
|
3558
|
+
expect(hs.status).toBe("unknown");
|
|
3559
|
+
});
|
|
3560
|
+
|
|
3561
|
+
it("healthStatus() reads integrity from the admin nested shape", async () => {
|
|
3562
|
+
const client = createTestClient();
|
|
3563
|
+
mockTokenResponse();
|
|
3564
|
+
mockJsonResponse({
|
|
3565
|
+
status: "ok",
|
|
3566
|
+
integrity: { healthy: true, manifest_load_failed: [] },
|
|
3567
|
+
});
|
|
3568
|
+
const hs = await client.healthStatus();
|
|
3569
|
+
expect(hs.status).toBe("ok");
|
|
3570
|
+
expect(hs.integrityOk).toBe(true);
|
|
3571
|
+
});
|
|
3572
|
+
|
|
3573
|
+
it("parseHealthStatus fails safe to degraded on a missing status", () => {
|
|
3574
|
+
const hs = parseHealthStatus({ integrity_ok: true });
|
|
3575
|
+
expect(hs.reachable).toBe(true);
|
|
3576
|
+
expect(hs.status).toBe("degraded");
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
it("parseHealthStatus yields an unknown snapshot for a non-object body", () => {
|
|
3580
|
+
const hs = parseHealthStatus("not json");
|
|
3581
|
+
expect(hs.reachable).toBe(false);
|
|
3582
|
+
expect(hs.status).toBe("unknown");
|
|
3583
|
+
});
|
|
3584
|
+
|
|
3585
|
+
it("JSON serialization is a safe summary that excludes detail", () => {
|
|
3586
|
+
const hs = parseHealthStatus({
|
|
3587
|
+
status: "degraded",
|
|
3588
|
+
integrity: {
|
|
3589
|
+
healthy: false,
|
|
3590
|
+
manifest_load_failed: ["secret_collection"],
|
|
3591
|
+
},
|
|
3592
|
+
});
|
|
3593
|
+
const json = JSON.stringify(hs);
|
|
3594
|
+
expect(json).not.toContain("detail");
|
|
3595
|
+
expect(json).not.toContain("secret_collection");
|
|
3596
|
+
const parsed = JSON.parse(json);
|
|
3597
|
+
expect(parsed).toEqual({
|
|
3598
|
+
reachable: true,
|
|
3599
|
+
status: "degraded",
|
|
3600
|
+
integrity_ok: false,
|
|
3601
|
+
});
|
|
3602
|
+
// detail is still readable in-process
|
|
3603
|
+
expect(hs.detail).toBeDefined();
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
it("parseHealthStatus preserves a raw off-contract status (matches Go)", () => {
|
|
3607
|
+
expect(parseHealthStatus({ status: "healthy" }).status).toBe("healthy");
|
|
3608
|
+
expect(parseHealthStatus({ status: "ok" }).status).toBe("ok");
|
|
3609
|
+
expect(parseHealthStatus({ status: "degraded" }).status).toBe("degraded");
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
it("parseHealthStatus fails safe to degraded on a non-string status", () => {
|
|
3613
|
+
expect(parseHealthStatus({ status: { nested: 1 } }).status).toBe(
|
|
3614
|
+
"degraded",
|
|
3615
|
+
);
|
|
3616
|
+
});
|
|
3617
|
+
|
|
3618
|
+
it("parseHealthStatus treats a non-object (array) body as unknown", () => {
|
|
3619
|
+
expect(parseHealthStatus([1, 2, 3]).reachable).toBe(false);
|
|
3620
|
+
expect(parseHealthStatus([1, 2, 3]).status).toBe("unknown");
|
|
3621
|
+
});
|
|
3622
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -119,6 +119,94 @@ export interface UpsertOptions {
|
|
|
119
119
|
bypassCache?: boolean;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A health status value. Like the Go client's `HealthState` (a string type), an
|
|
124
|
+
* off-contract status reported by the server is preserved verbatim.
|
|
125
|
+
*/
|
|
126
|
+
export type HealthState = string;
|
|
127
|
+
|
|
128
|
+
/** Canonical {@link HealthStatus.status} values. */
|
|
129
|
+
export const HealthOK = "ok";
|
|
130
|
+
export const HealthDegraded = "degraded";
|
|
131
|
+
export const HealthUnknown = "unknown";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A snapshot of an ekoDB /api/health probe.
|
|
135
|
+
*
|
|
136
|
+
* It is degraded-tolerant: a reachable server that reports `degraded` is a
|
|
137
|
+
* successful snapshot (`reachable: true`, `status: "degraded"`), NOT an error.
|
|
138
|
+
* An unreachable/unparseable probe yields `{ reachable: false, status: "unknown" }`.
|
|
139
|
+
*
|
|
140
|
+
* Consumers base liveness on `reachable` and treat `degraded` as a warning,
|
|
141
|
+
* never as a fatal. `detail` (the full admin body, which includes internal
|
|
142
|
+
* metrics and collection names) is excluded from JSON via `toJSON()` so
|
|
143
|
+
* surfacing the snapshot cannot leak internals; read it in-process when needed.
|
|
144
|
+
*/
|
|
145
|
+
export class HealthStatus {
|
|
146
|
+
reachable: boolean;
|
|
147
|
+
status: HealthState;
|
|
148
|
+
integrityOk: boolean;
|
|
149
|
+
detail?: { [key: string]: any };
|
|
150
|
+
|
|
151
|
+
constructor(init: {
|
|
152
|
+
reachable: boolean;
|
|
153
|
+
status: HealthState;
|
|
154
|
+
integrityOk: boolean;
|
|
155
|
+
detail?: { [key: string]: any };
|
|
156
|
+
}) {
|
|
157
|
+
this.reachable = init.reachable;
|
|
158
|
+
this.status = init.status;
|
|
159
|
+
this.integrityOk = init.integrityOk;
|
|
160
|
+
this.detail = init.detail;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
toJSON(): { reachable: boolean; status: HealthState; integrity_ok: boolean } {
|
|
164
|
+
return {
|
|
165
|
+
reachable: this.reachable,
|
|
166
|
+
status: this.status,
|
|
167
|
+
integrity_ok: this.integrityOk,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Interprets a parsed /api/health body per the shared health contract. A
|
|
174
|
+
* missing/odd `status` on a reachable body fails safe to `degraded`; a
|
|
175
|
+
* non-object body yields an unreachable `unknown` snapshot. `integrity_ok` is
|
|
176
|
+
* read from the top-level field (public) or nested `integrity.healthy` (admin).
|
|
177
|
+
*/
|
|
178
|
+
export function parseHealthStatus(body: any): HealthStatus {
|
|
179
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
180
|
+
return new HealthStatus({
|
|
181
|
+
reachable: false,
|
|
182
|
+
status: HealthUnknown,
|
|
183
|
+
integrityOk: false,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Default to degraded; a non-empty string status is kept verbatim (matches
|
|
187
|
+
// the Go client). A missing/non-string status fails safe to degraded.
|
|
188
|
+
let status: HealthState = HealthDegraded;
|
|
189
|
+
if (typeof body.status === "string" && body.status !== "") {
|
|
190
|
+
status = body.status;
|
|
191
|
+
}
|
|
192
|
+
let integrityOk = false;
|
|
193
|
+
if (typeof body.integrity_ok === "boolean") {
|
|
194
|
+
integrityOk = body.integrity_ok;
|
|
195
|
+
} else if (
|
|
196
|
+
body.integrity &&
|
|
197
|
+
typeof body.integrity === "object" &&
|
|
198
|
+
typeof body.integrity.healthy === "boolean"
|
|
199
|
+
) {
|
|
200
|
+
integrityOk = body.integrity.healthy;
|
|
201
|
+
}
|
|
202
|
+
return new HealthStatus({
|
|
203
|
+
reachable: true,
|
|
204
|
+
status,
|
|
205
|
+
integrityOk,
|
|
206
|
+
detail: body,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
122
210
|
export interface FindOptions {
|
|
123
211
|
filter?: any;
|
|
124
212
|
sort?: any;
|
|
@@ -1827,20 +1915,39 @@ export class EkoDBClient {
|
|
|
1827
1915
|
}
|
|
1828
1916
|
|
|
1829
1917
|
/**
|
|
1830
|
-
* Health check -
|
|
1918
|
+
* Health check - reports whether the ekoDB server is reachable.
|
|
1919
|
+
*
|
|
1920
|
+
* Returns `true` whenever the server responds (INCLUDING when it reports
|
|
1921
|
+
* `degraded`) and `false` only when it is unreachable. The ekoDB server
|
|
1922
|
+
* returns HTTP 200 while degraded on purpose, so gating on `status === "ok"`
|
|
1923
|
+
* would treat a degraded-but-serving server as down. Use {@link healthStatus}
|
|
1924
|
+
* for the ok/degraded distinction.
|
|
1831
1925
|
*/
|
|
1832
1926
|
async health(): Promise<boolean> {
|
|
1927
|
+
return (await this.healthStatus()).reachable;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/**
|
|
1931
|
+
* Structured, degraded-tolerant health. Returns a {@link HealthStatus}
|
|
1932
|
+
* snapshot; an unreachable server yields `{ reachable: false, status:
|
|
1933
|
+
* "unknown" }` rather than throwing.
|
|
1934
|
+
*/
|
|
1935
|
+
async healthStatus(): Promise<HealthStatus> {
|
|
1833
1936
|
try {
|
|
1834
|
-
const
|
|
1937
|
+
const body = await this.makeRequest<any>(
|
|
1835
1938
|
"GET",
|
|
1836
1939
|
"/api/health",
|
|
1837
1940
|
undefined,
|
|
1838
1941
|
0,
|
|
1839
1942
|
true,
|
|
1840
1943
|
);
|
|
1841
|
-
return
|
|
1944
|
+
return parseHealthStatus(body);
|
|
1842
1945
|
} catch {
|
|
1843
|
-
return
|
|
1946
|
+
return new HealthStatus({
|
|
1947
|
+
reachable: false,
|
|
1948
|
+
status: HealthUnknown,
|
|
1949
|
+
integrityOk: false,
|
|
1950
|
+
});
|
|
1844
1951
|
}
|
|
1845
1952
|
}
|
|
1846
1953
|
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,13 @@ export {
|
|
|
7
7
|
RateLimitError,
|
|
8
8
|
SchemaCache,
|
|
9
9
|
extractRecordId,
|
|
10
|
+
HealthStatus,
|
|
11
|
+
parseHealthStatus,
|
|
12
|
+
HealthOK,
|
|
13
|
+
HealthDegraded,
|
|
14
|
+
HealthUnknown,
|
|
10
15
|
} from "./client";
|
|
16
|
+
export type { HealthState } from "./client";
|
|
11
17
|
export { QueryBuilder, SortOrder } from "./query-builder";
|
|
12
18
|
export { SearchQueryBuilder } from "./search";
|
|
13
19
|
export {
|