@ekodb/ekodb-client 0.14.0 → 0.15.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 +30 -1
- package/dist/client.d.ts +82 -0
- package/dist/client.js +272 -1
- package/dist/client.test.js +51 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/client.test.ts +74 -0
- package/src/client.ts +371 -0
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -328,9 +328,38 @@ const joinResults = await client.find("users", multiQuery);
|
|
|
328
328
|
|
|
329
329
|
### WebSocket Methods
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
**Full CRUD (14 methods):**
|
|
332
|
+
|
|
333
|
+
- `findAll(collection): Promise<Record[]>`
|
|
334
|
+
- `insert(collection, record, bypassRipple?): Promise<any>`
|
|
335
|
+
- `query(collection, options?): Promise<any[]>`
|
|
336
|
+
- `findById(collection, id): Promise<any>`
|
|
337
|
+
- `update(collection, id, record, bypassRipple?): Promise<any>`
|
|
338
|
+
- `delete(collection, id, bypassRipple?): Promise<void>`
|
|
339
|
+
- `batchInsert(collection, records, bypassRipple?): Promise<any>`
|
|
340
|
+
- `batchUpdate(collection, updates, bypassRipple?): Promise<any>`
|
|
341
|
+
- `batchDelete(collection, ids, bypassRipple?): Promise<void>`
|
|
342
|
+
- `textSearch(collection, query, fields?, limit?): Promise<any[]>`
|
|
343
|
+
- `distinctValues(collection, field, filter?): Promise<any>`
|
|
344
|
+
- `updateWithAction(collection, id, action, field, value?): Promise<any>`
|
|
345
|
+
- `createCollection(name, schema?): Promise<void>`
|
|
346
|
+
- `listCollections(): Promise<string[]>`
|
|
347
|
+
- `deleteCollection(name): Promise<void>`
|
|
332
348
|
- `close(): void`
|
|
333
349
|
|
|
350
|
+
**Schema Cache:**
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { SchemaCache, extractRecordId } from "@ekodb/ekodb-client";
|
|
354
|
+
|
|
355
|
+
const cache = new SchemaCache({ enabled: true, ttlSeconds: 300 });
|
|
356
|
+
ws.setSchemaCache(cache);
|
|
357
|
+
|
|
358
|
+
// Extract IDs correctly with custom primary_key_alias
|
|
359
|
+
const id = extractRecordId(record); // tries "id", "_id"
|
|
360
|
+
const id2 = ws.extractId("users", record); // uses cache
|
|
361
|
+
```
|
|
362
|
+
|
|
334
363
|
## Examples
|
|
335
364
|
|
|
336
365
|
See the
|
package/dist/client.d.ts
CHANGED
|
@@ -170,6 +170,7 @@ export interface ChatMessageRequest {
|
|
|
170
170
|
force_summarize?: boolean;
|
|
171
171
|
max_iterations?: number;
|
|
172
172
|
tool_config?: ToolConfig;
|
|
173
|
+
llm_model?: string;
|
|
173
174
|
}
|
|
174
175
|
export interface TokenUsage {
|
|
175
176
|
prompt_tokens: number;
|
|
@@ -766,6 +767,20 @@ export declare class EkoDBClient {
|
|
|
766
767
|
* Health check - verify the ekoDB server is responding
|
|
767
768
|
*/
|
|
768
769
|
health(): Promise<boolean>;
|
|
770
|
+
/**
|
|
771
|
+
* Execute a tool via ekoDB's server-side tool pipeline.
|
|
772
|
+
*
|
|
773
|
+
* Calls POST /api/chat/tools/execute which goes through the same
|
|
774
|
+
* execute_tool function as the LLM tool-calling loop — with all
|
|
775
|
+
* collection filtering, permission enforcement, and internal collection
|
|
776
|
+
* blocking. No LLM round-trip.
|
|
777
|
+
*
|
|
778
|
+
* @returns The tool result if executed, or null if the server doesn't
|
|
779
|
+
* support the endpoint (older ekoDB versions).
|
|
780
|
+
*/
|
|
781
|
+
executeTool(toolName: string, params: {
|
|
782
|
+
[key: string]: any;
|
|
783
|
+
}, chatId?: string): Promise<any | null>;
|
|
769
784
|
/**
|
|
770
785
|
* Create a new chat session
|
|
771
786
|
*/
|
|
@@ -1204,11 +1219,40 @@ export declare class EventStream<_T = unknown> {
|
|
|
1204
1219
|
/**
|
|
1205
1220
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1206
1221
|
*/
|
|
1222
|
+
/**
|
|
1223
|
+
* In-memory schema cache with TTL for primary_key_alias resolution.
|
|
1224
|
+
*/
|
|
1225
|
+
export declare class SchemaCache {
|
|
1226
|
+
private entries;
|
|
1227
|
+
private lruOrder;
|
|
1228
|
+
private enabled;
|
|
1229
|
+
private maxEntries;
|
|
1230
|
+
private ttlMs;
|
|
1231
|
+
constructor(options?: {
|
|
1232
|
+
enabled?: boolean;
|
|
1233
|
+
maxEntries?: number;
|
|
1234
|
+
ttlSeconds?: number;
|
|
1235
|
+
});
|
|
1236
|
+
isEnabled(): boolean;
|
|
1237
|
+
setEnabled(enabled: boolean): void;
|
|
1238
|
+
getAlias(collection: string): string | undefined;
|
|
1239
|
+
insert(collection: string, primaryKeyAlias: string, version: number): void;
|
|
1240
|
+
invalidate(collection: string): void;
|
|
1241
|
+
invalidateAll(): void;
|
|
1242
|
+
handleSchemaChanged(collection: string, version: number, primaryKeyAlias: string): void;
|
|
1243
|
+
get size(): number;
|
|
1244
|
+
private touchLRU;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Extract record ID from a record object, trying custom alias, "id", then "_id".
|
|
1248
|
+
*/
|
|
1249
|
+
export declare function extractRecordId(record: Record, extraCandidates?: string[]): string | undefined;
|
|
1207
1250
|
export declare class WebSocketClient {
|
|
1208
1251
|
private wsURL;
|
|
1209
1252
|
private token;
|
|
1210
1253
|
private ws;
|
|
1211
1254
|
private dispatcherRunning;
|
|
1255
|
+
private schemaCache;
|
|
1212
1256
|
private pendingRequests;
|
|
1213
1257
|
private subscriptions;
|
|
1214
1258
|
private chatStreams;
|
|
@@ -1254,6 +1298,44 @@ export declare class WebSocketClient {
|
|
|
1254
1298
|
* proxy timeouts.
|
|
1255
1299
|
*/
|
|
1256
1300
|
rawCompletion(request: RawCompletionRequest): Promise<RawCompletionResponse>;
|
|
1301
|
+
/** Attach a schema cache for automatic invalidation on SchemaChanged events. */
|
|
1302
|
+
setSchemaCache(cache: SchemaCache): void;
|
|
1303
|
+
/** Extract record ID using the schema cache's primary_key_alias. */
|
|
1304
|
+
extractId(collection: string, record: Record): string | undefined;
|
|
1305
|
+
private sendCRUD;
|
|
1306
|
+
/** Insert a single record via WebSocket. */
|
|
1307
|
+
insert(collection: string, record: Record, bypassRipple?: boolean): Promise<any>;
|
|
1308
|
+
/** Query records via WebSocket. */
|
|
1309
|
+
query(collection: string, options?: {
|
|
1310
|
+
filter?: any;
|
|
1311
|
+
sort?: any;
|
|
1312
|
+
limit?: number;
|
|
1313
|
+
skip?: number;
|
|
1314
|
+
}): Promise<any[]>;
|
|
1315
|
+
/** Find a record by ID via WebSocket. */
|
|
1316
|
+
findById(collection: string, id: string): Promise<any>;
|
|
1317
|
+
/** Update a record by ID via WebSocket. */
|
|
1318
|
+
update(collection: string, id: string, record: Record, bypassRipple?: boolean): Promise<any>;
|
|
1319
|
+
/** Delete a record by ID via WebSocket. */
|
|
1320
|
+
delete(collection: string, id: string, bypassRipple?: boolean): Promise<void>;
|
|
1321
|
+
/** Batch insert multiple records via WebSocket. */
|
|
1322
|
+
batchInsert(collection: string, records: Record[], bypassRipple?: boolean): Promise<any>;
|
|
1323
|
+
/** Batch update multiple records via WebSocket. */
|
|
1324
|
+
batchUpdate(collection: string, updates: [string, Record][], bypassRipple?: boolean): Promise<any>;
|
|
1325
|
+
/** Batch delete records by IDs via WebSocket. */
|
|
1326
|
+
batchDelete(collection: string, ids: string[], bypassRipple?: boolean): Promise<void>;
|
|
1327
|
+
/** Full-text search via WebSocket. */
|
|
1328
|
+
textSearch(collection: string, query: string, fields?: string[], limit?: number): Promise<any[]>;
|
|
1329
|
+
/** Get distinct values for a field via WebSocket. */
|
|
1330
|
+
distinctValues(collection: string, field: string, filter?: any): Promise<any>;
|
|
1331
|
+
/** Apply an atomic field action via WebSocket. */
|
|
1332
|
+
updateWithAction(collection: string, id: string, action: string, field: string, value?: any): Promise<any>;
|
|
1333
|
+
/** Create a collection with optional schema via WebSocket. */
|
|
1334
|
+
createCollection(name: string, schema?: any): Promise<void>;
|
|
1335
|
+
/** List all collections via WebSocket. */
|
|
1336
|
+
listCollections(): Promise<string[]>;
|
|
1337
|
+
/** Delete a collection via WebSocket. */
|
|
1338
|
+
deleteCollection(name: string): Promise<void>;
|
|
1257
1339
|
/**
|
|
1258
1340
|
* Close the WebSocket connection.
|
|
1259
1341
|
*/
|
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.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
|
|
39
|
+
exports.WebSocketClient = exports.SchemaCache = exports.EventStream = exports.EkoDBClient = exports.MergeStrategy = exports.RateLimitError = exports.SerializationFormat = void 0;
|
|
40
|
+
exports.extractRecordId = extractRecordId;
|
|
40
41
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
41
42
|
const query_builder_1 = require("./query-builder");
|
|
42
43
|
const schema_1 = require("./schema");
|
|
@@ -930,6 +931,43 @@ class EkoDBClient {
|
|
|
930
931
|
}
|
|
931
932
|
}
|
|
932
933
|
// ========== Chat Methods ==========
|
|
934
|
+
/**
|
|
935
|
+
* Execute a tool via ekoDB's server-side tool pipeline.
|
|
936
|
+
*
|
|
937
|
+
* Calls POST /api/chat/tools/execute which goes through the same
|
|
938
|
+
* execute_tool function as the LLM tool-calling loop — with all
|
|
939
|
+
* collection filtering, permission enforcement, and internal collection
|
|
940
|
+
* blocking. No LLM round-trip.
|
|
941
|
+
*
|
|
942
|
+
* @returns The tool result if executed, or null if the server doesn't
|
|
943
|
+
* support the endpoint (older ekoDB versions).
|
|
944
|
+
*/
|
|
945
|
+
async executeTool(toolName, params, chatId) {
|
|
946
|
+
const body = { tool: toolName, params };
|
|
947
|
+
if (chatId) {
|
|
948
|
+
body.chat_id = chatId;
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
const result = await this.makeRequest("POST", "/api/chat/tools/execute", body, 0, true);
|
|
952
|
+
if (result.success) {
|
|
953
|
+
return result.result;
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
throw new Error(result.error || "tool execution failed");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
// Server doesn't have the endpoint (404) or route mismatch (405)
|
|
961
|
+
// Parse status from makeRequest error format: "Request failed with status NNN: ..."
|
|
962
|
+
const message = String(err?.message ?? "");
|
|
963
|
+
const match = message.match(/Request failed with status (\d+):/);
|
|
964
|
+
const status = match ? parseInt(match[1], 10) : undefined;
|
|
965
|
+
if (status === 404 || status === 405) {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
throw err;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
933
971
|
/**
|
|
934
972
|
* Create a new chat session
|
|
935
973
|
*/
|
|
@@ -1741,10 +1779,107 @@ exports.EventStream = EventStream;
|
|
|
1741
1779
|
/**
|
|
1742
1780
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1743
1781
|
*/
|
|
1782
|
+
/**
|
|
1783
|
+
* In-memory schema cache with TTL for primary_key_alias resolution.
|
|
1784
|
+
*/
|
|
1785
|
+
class SchemaCache {
|
|
1786
|
+
constructor(options = {}) {
|
|
1787
|
+
this.entries = new Map();
|
|
1788
|
+
this.lruOrder = [];
|
|
1789
|
+
this.enabled = options.enabled ?? false;
|
|
1790
|
+
this.maxEntries = options.maxEntries ?? 100;
|
|
1791
|
+
this.ttlMs = (options.ttlSeconds ?? 300) * 1000;
|
|
1792
|
+
}
|
|
1793
|
+
isEnabled() {
|
|
1794
|
+
return this.enabled;
|
|
1795
|
+
}
|
|
1796
|
+
setEnabled(enabled) {
|
|
1797
|
+
this.enabled = enabled;
|
|
1798
|
+
}
|
|
1799
|
+
getAlias(collection) {
|
|
1800
|
+
if (!this.enabled)
|
|
1801
|
+
return undefined;
|
|
1802
|
+
const entry = this.entries.get(collection);
|
|
1803
|
+
if (!entry)
|
|
1804
|
+
return undefined;
|
|
1805
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
1806
|
+
this.entries.delete(collection);
|
|
1807
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1808
|
+
return undefined;
|
|
1809
|
+
}
|
|
1810
|
+
this.touchLRU(collection);
|
|
1811
|
+
return entry.primaryKeyAlias;
|
|
1812
|
+
}
|
|
1813
|
+
insert(collection, primaryKeyAlias, version) {
|
|
1814
|
+
if (!this.enabled)
|
|
1815
|
+
return;
|
|
1816
|
+
const isNew = !this.entries.has(collection);
|
|
1817
|
+
this.entries.set(collection, {
|
|
1818
|
+
primaryKeyAlias,
|
|
1819
|
+
version,
|
|
1820
|
+
cachedAt: Date.now(),
|
|
1821
|
+
});
|
|
1822
|
+
if (isNew) {
|
|
1823
|
+
this.lruOrder.push(collection);
|
|
1824
|
+
while (this.lruOrder.length > this.maxEntries) {
|
|
1825
|
+
const oldest = this.lruOrder.shift();
|
|
1826
|
+
this.entries.delete(oldest);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
else {
|
|
1830
|
+
this.touchLRU(collection);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
invalidate(collection) {
|
|
1834
|
+
this.entries.delete(collection);
|
|
1835
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1836
|
+
}
|
|
1837
|
+
invalidateAll() {
|
|
1838
|
+
this.entries.clear();
|
|
1839
|
+
this.lruOrder = [];
|
|
1840
|
+
}
|
|
1841
|
+
handleSchemaChanged(collection, version, primaryKeyAlias) {
|
|
1842
|
+
if (!this.enabled)
|
|
1843
|
+
return;
|
|
1844
|
+
const existing = this.entries.get(collection);
|
|
1845
|
+
if (existing && version <= existing.version)
|
|
1846
|
+
return;
|
|
1847
|
+
this.insert(collection, primaryKeyAlias, version);
|
|
1848
|
+
}
|
|
1849
|
+
get size() {
|
|
1850
|
+
return this.entries.size;
|
|
1851
|
+
}
|
|
1852
|
+
touchLRU(collection) {
|
|
1853
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1854
|
+
this.lruOrder.push(collection);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
exports.SchemaCache = SchemaCache;
|
|
1858
|
+
/**
|
|
1859
|
+
* Extract record ID from a record object, trying custom alias, "id", then "_id".
|
|
1860
|
+
*/
|
|
1861
|
+
function extractRecordId(record, extraCandidates = []) {
|
|
1862
|
+
for (const key of extraCandidates) {
|
|
1863
|
+
const val = record[key];
|
|
1864
|
+
if (typeof val === "string")
|
|
1865
|
+
return val;
|
|
1866
|
+
if (val && typeof val === "object" && "value" in val)
|
|
1867
|
+
return String(val.value);
|
|
1868
|
+
}
|
|
1869
|
+
for (const key of ["id", "_id"]) {
|
|
1870
|
+
const val = record[key];
|
|
1871
|
+
if (typeof val === "string")
|
|
1872
|
+
return val;
|
|
1873
|
+
if (val && typeof val === "object" && "value" in val)
|
|
1874
|
+
return String(val.value);
|
|
1875
|
+
}
|
|
1876
|
+
return undefined;
|
|
1877
|
+
}
|
|
1744
1878
|
class WebSocketClient {
|
|
1745
1879
|
constructor(wsURL, token) {
|
|
1746
1880
|
this.ws = null;
|
|
1747
1881
|
this.dispatcherRunning = false;
|
|
1882
|
+
this.schemaCache = null;
|
|
1748
1883
|
// Dispatcher state
|
|
1749
1884
|
this.pendingRequests = new Map();
|
|
1750
1885
|
this.subscriptions = new Map();
|
|
@@ -1919,6 +2054,12 @@ class WebSocketClient {
|
|
|
1919
2054
|
}
|
|
1920
2055
|
break;
|
|
1921
2056
|
}
|
|
2057
|
+
case "SchemaChanged": {
|
|
2058
|
+
if (this.schemaCache && msg.payload) {
|
|
2059
|
+
this.schemaCache.handleSchemaChanged(msg.payload.collection, msg.payload.version, msg.payload.primary_key_alias);
|
|
2060
|
+
}
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
1922
2063
|
case "ClientToolCall": {
|
|
1923
2064
|
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1924
2065
|
const stream = this.chatStreams.get(chatId);
|
|
@@ -2083,6 +2224,136 @@ class WebSocketClient {
|
|
|
2083
2224
|
});
|
|
2084
2225
|
return { content: payload?.data?.content || "" };
|
|
2085
2226
|
}
|
|
2227
|
+
/** Attach a schema cache for automatic invalidation on SchemaChanged events. */
|
|
2228
|
+
setSchemaCache(cache) {
|
|
2229
|
+
this.schemaCache = cache;
|
|
2230
|
+
}
|
|
2231
|
+
/** Extract record ID using the schema cache's primary_key_alias. */
|
|
2232
|
+
extractId(collection, record) {
|
|
2233
|
+
const alias = this.schemaCache?.getAlias(collection);
|
|
2234
|
+
return extractRecordId(record, alias ? [alias] : []);
|
|
2235
|
+
}
|
|
2236
|
+
// =========================================================================
|
|
2237
|
+
// WS CRUD Methods — Full Parity with Server
|
|
2238
|
+
// =========================================================================
|
|
2239
|
+
async sendCRUD(msgType, payload) {
|
|
2240
|
+
const messageId = this.genMessageId();
|
|
2241
|
+
const response = await this.sendRequest({
|
|
2242
|
+
type: msgType,
|
|
2243
|
+
messageId,
|
|
2244
|
+
payload,
|
|
2245
|
+
});
|
|
2246
|
+
return response?.data ?? response;
|
|
2247
|
+
}
|
|
2248
|
+
/** Insert a single record via WebSocket. */
|
|
2249
|
+
async insert(collection, record, bypassRipple) {
|
|
2250
|
+
return this.sendCRUD("Insert", {
|
|
2251
|
+
collection,
|
|
2252
|
+
record,
|
|
2253
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
/** Query records via WebSocket. */
|
|
2257
|
+
async query(collection, options) {
|
|
2258
|
+
const data = await this.sendCRUD("Query", {
|
|
2259
|
+
collection,
|
|
2260
|
+
...options,
|
|
2261
|
+
});
|
|
2262
|
+
return Array.isArray(data) ? data : [];
|
|
2263
|
+
}
|
|
2264
|
+
/** Find a record by ID via WebSocket. */
|
|
2265
|
+
async findById(collection, id) {
|
|
2266
|
+
return this.sendCRUD("FindById", { collection, id });
|
|
2267
|
+
}
|
|
2268
|
+
/** Update a record by ID via WebSocket. */
|
|
2269
|
+
async update(collection, id, record, bypassRipple) {
|
|
2270
|
+
return this.sendCRUD("Update", {
|
|
2271
|
+
collection,
|
|
2272
|
+
id,
|
|
2273
|
+
record,
|
|
2274
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
/** Delete a record by ID via WebSocket. */
|
|
2278
|
+
async delete(collection, id, bypassRipple) {
|
|
2279
|
+
await this.sendCRUD("Delete", {
|
|
2280
|
+
collection,
|
|
2281
|
+
id,
|
|
2282
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
/** Batch insert multiple records via WebSocket. */
|
|
2286
|
+
async batchInsert(collection, records, bypassRipple) {
|
|
2287
|
+
return this.sendCRUD("BatchInsert", {
|
|
2288
|
+
collection,
|
|
2289
|
+
records,
|
|
2290
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
/** Batch update multiple records via WebSocket. */
|
|
2294
|
+
async batchUpdate(collection, updates, bypassRipple) {
|
|
2295
|
+
return this.sendCRUD("BatchUpdate", {
|
|
2296
|
+
collection,
|
|
2297
|
+
updates,
|
|
2298
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
/** Batch delete records by IDs via WebSocket. */
|
|
2302
|
+
async batchDelete(collection, ids, bypassRipple) {
|
|
2303
|
+
await this.sendCRUD("BatchDelete", {
|
|
2304
|
+
collection,
|
|
2305
|
+
ids,
|
|
2306
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
/** Full-text search via WebSocket. */
|
|
2310
|
+
async textSearch(collection, query, fields, limit) {
|
|
2311
|
+
const options = {};
|
|
2312
|
+
if (fields)
|
|
2313
|
+
options.fields = fields;
|
|
2314
|
+
if (limit)
|
|
2315
|
+
options.limit = limit;
|
|
2316
|
+
const data = await this.sendCRUD("TextSearch", {
|
|
2317
|
+
collection,
|
|
2318
|
+
query,
|
|
2319
|
+
...(Object.keys(options).length > 0 && { options }),
|
|
2320
|
+
});
|
|
2321
|
+
return Array.isArray(data) ? data : [];
|
|
2322
|
+
}
|
|
2323
|
+
/** Get distinct values for a field via WebSocket. */
|
|
2324
|
+
async distinctValues(collection, field, filter) {
|
|
2325
|
+
return this.sendCRUD("DistinctValues", {
|
|
2326
|
+
collection,
|
|
2327
|
+
field,
|
|
2328
|
+
...(filter && { filter }),
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
/** Apply an atomic field action via WebSocket. */
|
|
2332
|
+
async updateWithAction(collection, id, action, field, value) {
|
|
2333
|
+
return this.sendCRUD("UpdateWithAction", {
|
|
2334
|
+
collection,
|
|
2335
|
+
id,
|
|
2336
|
+
action,
|
|
2337
|
+
field,
|
|
2338
|
+
...(value !== undefined && { value }),
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
/** Create a collection with optional schema via WebSocket. */
|
|
2342
|
+
async createCollection(name, schema) {
|
|
2343
|
+
await this.sendCRUD("CreateCollection", {
|
|
2344
|
+
name,
|
|
2345
|
+
schema: schema ?? {},
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
/** List all collections via WebSocket. */
|
|
2349
|
+
async listCollections() {
|
|
2350
|
+
const data = await this.sendCRUD("GetCollections", {});
|
|
2351
|
+
return Array.isArray(data) ? data : [];
|
|
2352
|
+
}
|
|
2353
|
+
/** Delete a collection via WebSocket. */
|
|
2354
|
+
async deleteCollection(name) {
|
|
2355
|
+
await this.sendCRUD("DeleteCollection", { name });
|
|
2356
|
+
}
|
|
2086
2357
|
/**
|
|
2087
2358
|
* Close the WebSocket connection.
|
|
2088
2359
|
*/
|
package/dist/client.test.js
CHANGED
|
@@ -805,6 +805,57 @@ function mockErrorResponse(status, message) {
|
|
|
805
805
|
});
|
|
806
806
|
});
|
|
807
807
|
// ============================================================================
|
|
808
|
+
// executeTool Tests
|
|
809
|
+
// ============================================================================
|
|
810
|
+
(0, vitest_1.describe)("EkoDBClient executeTool", () => {
|
|
811
|
+
(0, vitest_1.it)("executes tool successfully", async () => {
|
|
812
|
+
const client = createTestClient();
|
|
813
|
+
mockTokenResponse();
|
|
814
|
+
mockJsonResponse({
|
|
815
|
+
success: true,
|
|
816
|
+
result: { count: 42 },
|
|
817
|
+
});
|
|
818
|
+
const result = await client.executeTool("count_records", {
|
|
819
|
+
collection: "users",
|
|
820
|
+
});
|
|
821
|
+
(0, vitest_1.expect)(result).toEqual({ count: 42 });
|
|
822
|
+
});
|
|
823
|
+
(0, vitest_1.it)("passes chat_id when provided", async () => {
|
|
824
|
+
const client = createTestClient();
|
|
825
|
+
mockTokenResponse();
|
|
826
|
+
mockJsonResponse({
|
|
827
|
+
success: true,
|
|
828
|
+
result: { value: "hello" },
|
|
829
|
+
});
|
|
830
|
+
const result = await client.executeTool("kv_get", { key: "greeting" }, "chat_456");
|
|
831
|
+
(0, vitest_1.expect)(result).toEqual({ value: "hello" });
|
|
832
|
+
// Verify the request body included chat_id
|
|
833
|
+
const lastCall = mockFetch.mock.calls[1];
|
|
834
|
+
const body = JSON.parse(lastCall[1].body);
|
|
835
|
+
(0, vitest_1.expect)(body.chat_id).toBe("chat_456");
|
|
836
|
+
(0, vitest_1.expect)(body.tool).toBe("kv_get");
|
|
837
|
+
(0, vitest_1.expect)(body.params).toEqual({ key: "greeting" });
|
|
838
|
+
});
|
|
839
|
+
(0, vitest_1.it)("throws on tool execution failure", async () => {
|
|
840
|
+
const client = createTestClient();
|
|
841
|
+
mockTokenResponse();
|
|
842
|
+
mockJsonResponse({
|
|
843
|
+
success: false,
|
|
844
|
+
error: "permission denied",
|
|
845
|
+
});
|
|
846
|
+
await (0, vitest_1.expect)(client.executeTool("delete_collection", { collection: "system" })).rejects.toThrow("permission denied");
|
|
847
|
+
});
|
|
848
|
+
(0, vitest_1.it)("returns null when server does not support endpoint", async () => {
|
|
849
|
+
const client = createTestClient();
|
|
850
|
+
mockTokenResponse();
|
|
851
|
+
mockErrorResponse(404, "Not Found");
|
|
852
|
+
const result = await client.executeTool("count_records", {
|
|
853
|
+
collection: "users",
|
|
854
|
+
});
|
|
855
|
+
(0, vitest_1.expect)(result).toBeNull();
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
// ============================================================================
|
|
808
859
|
// Chat Models Tests
|
|
809
860
|
// ============================================================================
|
|
810
861
|
(0, vitest_1.describe)("EkoDBClient chat models", () => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { EkoDBClient, WebSocketClient, EventStream, SerializationFormat, MergeStrategy, RateLimitError, } from "./client";
|
|
1
|
+
export { EkoDBClient, WebSocketClient, EventStream, SerializationFormat, MergeStrategy, RateLimitError, SchemaCache, extractRecordId, } from "./client";
|
|
2
2
|
export { QueryBuilder, SortOrder } from "./query-builder";
|
|
3
3
|
export { SearchQueryBuilder } from "./search";
|
|
4
4
|
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.ChatMessage = exports.Stage = exports.JoinBuilder = exports.DistanceMetric = exports.VectorIndexAlgorithm = exports.FieldTypeSchemaBuilder = exports.SchemaBuilder = exports.SearchQueryBuilder = exports.SortOrder = exports.QueryBuilder = 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.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;
|
|
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; } });
|
|
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "EventStream", { enumerable: true, get: function
|
|
|
8
8
|
Object.defineProperty(exports, "SerializationFormat", { enumerable: true, get: function () { return client_1.SerializationFormat; } });
|
|
9
9
|
Object.defineProperty(exports, "MergeStrategy", { enumerable: true, get: function () { return client_1.MergeStrategy; } });
|
|
10
10
|
Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return client_1.RateLimitError; } });
|
|
11
|
+
Object.defineProperty(exports, "SchemaCache", { enumerable: true, get: function () { return client_1.SchemaCache; } });
|
|
12
|
+
Object.defineProperty(exports, "extractRecordId", { enumerable: true, get: function () { return client_1.extractRecordId; } });
|
|
11
13
|
var query_builder_1 = require("./query-builder");
|
|
12
14
|
Object.defineProperty(exports, "QueryBuilder", { enumerable: true, get: function () { return query_builder_1.QueryBuilder; } });
|
|
13
15
|
Object.defineProperty(exports, "SortOrder", { enumerable: true, get: function () { return query_builder_1.SortOrder; } });
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -1105,6 +1105,80 @@ describe("Convenience methods", () => {
|
|
|
1105
1105
|
});
|
|
1106
1106
|
});
|
|
1107
1107
|
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
// executeTool Tests
|
|
1110
|
+
// ============================================================================
|
|
1111
|
+
|
|
1112
|
+
describe("EkoDBClient executeTool", () => {
|
|
1113
|
+
it("executes tool successfully", async () => {
|
|
1114
|
+
const client = createTestClient();
|
|
1115
|
+
|
|
1116
|
+
mockTokenResponse();
|
|
1117
|
+
mockJsonResponse({
|
|
1118
|
+
success: true,
|
|
1119
|
+
result: { count: 42 },
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
const result = await client.executeTool("count_records", {
|
|
1123
|
+
collection: "users",
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
expect(result).toEqual({ count: 42 });
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it("passes chat_id when provided", async () => {
|
|
1130
|
+
const client = createTestClient();
|
|
1131
|
+
|
|
1132
|
+
mockTokenResponse();
|
|
1133
|
+
mockJsonResponse({
|
|
1134
|
+
success: true,
|
|
1135
|
+
result: { value: "hello" },
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
const result = await client.executeTool(
|
|
1139
|
+
"kv_get",
|
|
1140
|
+
{ key: "greeting" },
|
|
1141
|
+
"chat_456",
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
expect(result).toEqual({ value: "hello" });
|
|
1145
|
+
|
|
1146
|
+
// Verify the request body included chat_id
|
|
1147
|
+
const lastCall = mockFetch.mock.calls[1];
|
|
1148
|
+
const body = JSON.parse(lastCall[1].body);
|
|
1149
|
+
expect(body.chat_id).toBe("chat_456");
|
|
1150
|
+
expect(body.tool).toBe("kv_get");
|
|
1151
|
+
expect(body.params).toEqual({ key: "greeting" });
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
it("throws on tool execution failure", async () => {
|
|
1155
|
+
const client = createTestClient();
|
|
1156
|
+
|
|
1157
|
+
mockTokenResponse();
|
|
1158
|
+
mockJsonResponse({
|
|
1159
|
+
success: false,
|
|
1160
|
+
error: "permission denied",
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
await expect(
|
|
1164
|
+
client.executeTool("delete_collection", { collection: "system" }),
|
|
1165
|
+
).rejects.toThrow("permission denied");
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("returns null when server does not support endpoint", async () => {
|
|
1169
|
+
const client = createTestClient();
|
|
1170
|
+
|
|
1171
|
+
mockTokenResponse();
|
|
1172
|
+
mockErrorResponse(404, "Not Found");
|
|
1173
|
+
|
|
1174
|
+
const result = await client.executeTool("count_records", {
|
|
1175
|
+
collection: "users",
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
expect(result).toBeNull();
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1108
1182
|
// ============================================================================
|
|
1109
1183
|
// Chat Models Tests
|
|
1110
1184
|
// ============================================================================
|
package/src/client.ts
CHANGED
|
@@ -203,6 +203,7 @@ export interface ChatMessageRequest {
|
|
|
203
203
|
force_summarize?: boolean;
|
|
204
204
|
max_iterations?: number;
|
|
205
205
|
tool_config?: ToolConfig;
|
|
206
|
+
llm_model?: string;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
export interface TokenUsage {
|
|
@@ -1552,6 +1553,54 @@ export class EkoDBClient {
|
|
|
1552
1553
|
|
|
1553
1554
|
// ========== Chat Methods ==========
|
|
1554
1555
|
|
|
1556
|
+
/**
|
|
1557
|
+
* Execute a tool via ekoDB's server-side tool pipeline.
|
|
1558
|
+
*
|
|
1559
|
+
* Calls POST /api/chat/tools/execute which goes through the same
|
|
1560
|
+
* execute_tool function as the LLM tool-calling loop — with all
|
|
1561
|
+
* collection filtering, permission enforcement, and internal collection
|
|
1562
|
+
* blocking. No LLM round-trip.
|
|
1563
|
+
*
|
|
1564
|
+
* @returns The tool result if executed, or null if the server doesn't
|
|
1565
|
+
* support the endpoint (older ekoDB versions).
|
|
1566
|
+
*/
|
|
1567
|
+
async executeTool(
|
|
1568
|
+
toolName: string,
|
|
1569
|
+
params: { [key: string]: any },
|
|
1570
|
+
chatId?: string,
|
|
1571
|
+
): Promise<any | null> {
|
|
1572
|
+
const body: { [key: string]: any } = { tool: toolName, params };
|
|
1573
|
+
if (chatId) {
|
|
1574
|
+
body.chat_id = chatId;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
try {
|
|
1578
|
+
const result = await this.makeRequest<{ [key: string]: any }>(
|
|
1579
|
+
"POST",
|
|
1580
|
+
"/api/chat/tools/execute",
|
|
1581
|
+
body,
|
|
1582
|
+
0,
|
|
1583
|
+
true, // Force JSON for chat operations
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
if (result.success) {
|
|
1587
|
+
return result.result;
|
|
1588
|
+
} else {
|
|
1589
|
+
throw new Error(result.error || "tool execution failed");
|
|
1590
|
+
}
|
|
1591
|
+
} catch (err: any) {
|
|
1592
|
+
// Server doesn't have the endpoint (404) or route mismatch (405)
|
|
1593
|
+
// Parse status from makeRequest error format: "Request failed with status NNN: ..."
|
|
1594
|
+
const message = String(err?.message ?? "");
|
|
1595
|
+
const match = message.match(/Request failed with status (\d+):/);
|
|
1596
|
+
const status = match ? parseInt(match[1], 10) : undefined;
|
|
1597
|
+
if (status === 404 || status === 405) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
throw err;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1555
1604
|
/**
|
|
1556
1605
|
* Create a new chat session
|
|
1557
1606
|
*/
|
|
@@ -2977,11 +3026,128 @@ export class EventStream<_T = unknown> {
|
|
|
2977
3026
|
/**
|
|
2978
3027
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
2979
3028
|
*/
|
|
3029
|
+
/**
|
|
3030
|
+
* In-memory schema cache with TTL for primary_key_alias resolution.
|
|
3031
|
+
*/
|
|
3032
|
+
export class SchemaCache {
|
|
3033
|
+
private entries: Map<
|
|
3034
|
+
string,
|
|
3035
|
+
{ primaryKeyAlias: string; version: number; cachedAt: number }
|
|
3036
|
+
> = new Map();
|
|
3037
|
+
private lruOrder: string[] = [];
|
|
3038
|
+
private enabled: boolean;
|
|
3039
|
+
private maxEntries: number;
|
|
3040
|
+
private ttlMs: number;
|
|
3041
|
+
|
|
3042
|
+
constructor(
|
|
3043
|
+
options: {
|
|
3044
|
+
enabled?: boolean;
|
|
3045
|
+
maxEntries?: number;
|
|
3046
|
+
ttlSeconds?: number;
|
|
3047
|
+
} = {},
|
|
3048
|
+
) {
|
|
3049
|
+
this.enabled = options.enabled ?? false;
|
|
3050
|
+
this.maxEntries = options.maxEntries ?? 100;
|
|
3051
|
+
this.ttlMs = (options.ttlSeconds ?? 300) * 1000;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
isEnabled(): boolean {
|
|
3055
|
+
return this.enabled;
|
|
3056
|
+
}
|
|
3057
|
+
setEnabled(enabled: boolean): void {
|
|
3058
|
+
this.enabled = enabled;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
getAlias(collection: string): string | undefined {
|
|
3062
|
+
if (!this.enabled) return undefined;
|
|
3063
|
+
const entry = this.entries.get(collection);
|
|
3064
|
+
if (!entry) return undefined;
|
|
3065
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
3066
|
+
this.entries.delete(collection);
|
|
3067
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3068
|
+
return undefined;
|
|
3069
|
+
}
|
|
3070
|
+
this.touchLRU(collection);
|
|
3071
|
+
return entry.primaryKeyAlias;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
insert(collection: string, primaryKeyAlias: string, version: number): void {
|
|
3075
|
+
if (!this.enabled) return;
|
|
3076
|
+
const isNew = !this.entries.has(collection);
|
|
3077
|
+
this.entries.set(collection, {
|
|
3078
|
+
primaryKeyAlias,
|
|
3079
|
+
version,
|
|
3080
|
+
cachedAt: Date.now(),
|
|
3081
|
+
});
|
|
3082
|
+
if (isNew) {
|
|
3083
|
+
this.lruOrder.push(collection);
|
|
3084
|
+
while (this.lruOrder.length > this.maxEntries) {
|
|
3085
|
+
const oldest = this.lruOrder.shift()!;
|
|
3086
|
+
this.entries.delete(oldest);
|
|
3087
|
+
}
|
|
3088
|
+
} else {
|
|
3089
|
+
this.touchLRU(collection);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
invalidate(collection: string): void {
|
|
3094
|
+
this.entries.delete(collection);
|
|
3095
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3096
|
+
}
|
|
3097
|
+
invalidateAll(): void {
|
|
3098
|
+
this.entries.clear();
|
|
3099
|
+
this.lruOrder = [];
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
handleSchemaChanged(
|
|
3103
|
+
collection: string,
|
|
3104
|
+
version: number,
|
|
3105
|
+
primaryKeyAlias: string,
|
|
3106
|
+
): void {
|
|
3107
|
+
if (!this.enabled) return;
|
|
3108
|
+
const existing = this.entries.get(collection);
|
|
3109
|
+
if (existing && version <= existing.version) return;
|
|
3110
|
+
this.insert(collection, primaryKeyAlias, version);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
get size(): number {
|
|
3114
|
+
return this.entries.size;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
private touchLRU(collection: string): void {
|
|
3118
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3119
|
+
this.lruOrder.push(collection);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Extract record ID from a record object, trying custom alias, "id", then "_id".
|
|
3125
|
+
*/
|
|
3126
|
+
export function extractRecordId(
|
|
3127
|
+
record: Record,
|
|
3128
|
+
extraCandidates: string[] = [],
|
|
3129
|
+
): string | undefined {
|
|
3130
|
+
for (const key of extraCandidates) {
|
|
3131
|
+
const val = record[key];
|
|
3132
|
+
if (typeof val === "string") return val;
|
|
3133
|
+
if (val && typeof val === "object" && "value" in val)
|
|
3134
|
+
return String(val.value);
|
|
3135
|
+
}
|
|
3136
|
+
for (const key of ["id", "_id"]) {
|
|
3137
|
+
const val = record[key];
|
|
3138
|
+
if (typeof val === "string") return val;
|
|
3139
|
+
if (val && typeof val === "object" && "value" in val)
|
|
3140
|
+
return String(val.value);
|
|
3141
|
+
}
|
|
3142
|
+
return undefined;
|
|
3143
|
+
}
|
|
3144
|
+
|
|
2980
3145
|
export class WebSocketClient {
|
|
2981
3146
|
private wsURL: string;
|
|
2982
3147
|
private token: string;
|
|
2983
3148
|
private ws: any = null;
|
|
2984
3149
|
private dispatcherRunning = false;
|
|
3150
|
+
private schemaCache: SchemaCache | null = null;
|
|
2985
3151
|
|
|
2986
3152
|
// Dispatcher state
|
|
2987
3153
|
private pendingRequests: Map<
|
|
@@ -3181,6 +3347,17 @@ export class WebSocketClient {
|
|
|
3181
3347
|
break;
|
|
3182
3348
|
}
|
|
3183
3349
|
|
|
3350
|
+
case "SchemaChanged": {
|
|
3351
|
+
if (this.schemaCache && msg.payload) {
|
|
3352
|
+
this.schemaCache.handleSchemaChanged(
|
|
3353
|
+
msg.payload.collection,
|
|
3354
|
+
msg.payload.version,
|
|
3355
|
+
msg.payload.primary_key_alias,
|
|
3356
|
+
);
|
|
3357
|
+
}
|
|
3358
|
+
break;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3184
3361
|
case "ClientToolCall": {
|
|
3185
3362
|
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3186
3363
|
const stream = this.chatStreams.get(chatId);
|
|
@@ -3382,6 +3559,200 @@ export class WebSocketClient {
|
|
|
3382
3559
|
return { content: payload?.data?.content || "" };
|
|
3383
3560
|
}
|
|
3384
3561
|
|
|
3562
|
+
/** Attach a schema cache for automatic invalidation on SchemaChanged events. */
|
|
3563
|
+
setSchemaCache(cache: SchemaCache): void {
|
|
3564
|
+
this.schemaCache = cache;
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
/** Extract record ID using the schema cache's primary_key_alias. */
|
|
3568
|
+
extractId(collection: string, record: Record): string | undefined {
|
|
3569
|
+
const alias = this.schemaCache?.getAlias(collection);
|
|
3570
|
+
return extractRecordId(record, alias ? [alias] : []);
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
// =========================================================================
|
|
3574
|
+
// WS CRUD Methods — Full Parity with Server
|
|
3575
|
+
// =========================================================================
|
|
3576
|
+
|
|
3577
|
+
private async sendCRUD(msgType: string, payload: any): Promise<any> {
|
|
3578
|
+
const messageId = this.genMessageId();
|
|
3579
|
+
const response = await this.sendRequest({
|
|
3580
|
+
type: msgType,
|
|
3581
|
+
messageId,
|
|
3582
|
+
payload,
|
|
3583
|
+
});
|
|
3584
|
+
return response?.data ?? response;
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
/** Insert a single record via WebSocket. */
|
|
3588
|
+
async insert(
|
|
3589
|
+
collection: string,
|
|
3590
|
+
record: Record,
|
|
3591
|
+
bypassRipple?: boolean,
|
|
3592
|
+
): Promise<any> {
|
|
3593
|
+
return this.sendCRUD("Insert", {
|
|
3594
|
+
collection,
|
|
3595
|
+
record,
|
|
3596
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/** Query records via WebSocket. */
|
|
3601
|
+
async query(
|
|
3602
|
+
collection: string,
|
|
3603
|
+
options?: {
|
|
3604
|
+
filter?: any;
|
|
3605
|
+
sort?: any;
|
|
3606
|
+
limit?: number;
|
|
3607
|
+
skip?: number;
|
|
3608
|
+
},
|
|
3609
|
+
): Promise<any[]> {
|
|
3610
|
+
const data = await this.sendCRUD("Query", {
|
|
3611
|
+
collection,
|
|
3612
|
+
...options,
|
|
3613
|
+
});
|
|
3614
|
+
return Array.isArray(data) ? data : [];
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
/** Find a record by ID via WebSocket. */
|
|
3618
|
+
async findById(collection: string, id: string): Promise<any> {
|
|
3619
|
+
return this.sendCRUD("FindById", { collection, id });
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
/** Update a record by ID via WebSocket. */
|
|
3623
|
+
async update(
|
|
3624
|
+
collection: string,
|
|
3625
|
+
id: string,
|
|
3626
|
+
record: Record,
|
|
3627
|
+
bypassRipple?: boolean,
|
|
3628
|
+
): Promise<any> {
|
|
3629
|
+
return this.sendCRUD("Update", {
|
|
3630
|
+
collection,
|
|
3631
|
+
id,
|
|
3632
|
+
record,
|
|
3633
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
/** Delete a record by ID via WebSocket. */
|
|
3638
|
+
async delete(
|
|
3639
|
+
collection: string,
|
|
3640
|
+
id: string,
|
|
3641
|
+
bypassRipple?: boolean,
|
|
3642
|
+
): Promise<void> {
|
|
3643
|
+
await this.sendCRUD("Delete", {
|
|
3644
|
+
collection,
|
|
3645
|
+
id,
|
|
3646
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
/** Batch insert multiple records via WebSocket. */
|
|
3651
|
+
async batchInsert(
|
|
3652
|
+
collection: string,
|
|
3653
|
+
records: Record[],
|
|
3654
|
+
bypassRipple?: boolean,
|
|
3655
|
+
): Promise<any> {
|
|
3656
|
+
return this.sendCRUD("BatchInsert", {
|
|
3657
|
+
collection,
|
|
3658
|
+
records,
|
|
3659
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
/** Batch update multiple records via WebSocket. */
|
|
3664
|
+
async batchUpdate(
|
|
3665
|
+
collection: string,
|
|
3666
|
+
updates: [string, Record][],
|
|
3667
|
+
bypassRipple?: boolean,
|
|
3668
|
+
): Promise<any> {
|
|
3669
|
+
return this.sendCRUD("BatchUpdate", {
|
|
3670
|
+
collection,
|
|
3671
|
+
updates,
|
|
3672
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3673
|
+
});
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
/** Batch delete records by IDs via WebSocket. */
|
|
3677
|
+
async batchDelete(
|
|
3678
|
+
collection: string,
|
|
3679
|
+
ids: string[],
|
|
3680
|
+
bypassRipple?: boolean,
|
|
3681
|
+
): Promise<void> {
|
|
3682
|
+
await this.sendCRUD("BatchDelete", {
|
|
3683
|
+
collection,
|
|
3684
|
+
ids,
|
|
3685
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
/** Full-text search via WebSocket. */
|
|
3690
|
+
async textSearch(
|
|
3691
|
+
collection: string,
|
|
3692
|
+
query: string,
|
|
3693
|
+
fields?: string[],
|
|
3694
|
+
limit?: number,
|
|
3695
|
+
): Promise<any[]> {
|
|
3696
|
+
const options: any = {};
|
|
3697
|
+
if (fields) options.fields = fields;
|
|
3698
|
+
if (limit) options.limit = limit;
|
|
3699
|
+
const data = await this.sendCRUD("TextSearch", {
|
|
3700
|
+
collection,
|
|
3701
|
+
query,
|
|
3702
|
+
...(Object.keys(options).length > 0 && { options }),
|
|
3703
|
+
});
|
|
3704
|
+
return Array.isArray(data) ? data : [];
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
/** Get distinct values for a field via WebSocket. */
|
|
3708
|
+
async distinctValues(
|
|
3709
|
+
collection: string,
|
|
3710
|
+
field: string,
|
|
3711
|
+
filter?: any,
|
|
3712
|
+
): Promise<any> {
|
|
3713
|
+
return this.sendCRUD("DistinctValues", {
|
|
3714
|
+
collection,
|
|
3715
|
+
field,
|
|
3716
|
+
...(filter && { filter }),
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
/** Apply an atomic field action via WebSocket. */
|
|
3721
|
+
async updateWithAction(
|
|
3722
|
+
collection: string,
|
|
3723
|
+
id: string,
|
|
3724
|
+
action: string,
|
|
3725
|
+
field: string,
|
|
3726
|
+
value?: any,
|
|
3727
|
+
): Promise<any> {
|
|
3728
|
+
return this.sendCRUD("UpdateWithAction", {
|
|
3729
|
+
collection,
|
|
3730
|
+
id,
|
|
3731
|
+
action,
|
|
3732
|
+
field,
|
|
3733
|
+
...(value !== undefined && { value }),
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
/** Create a collection with optional schema via WebSocket. */
|
|
3738
|
+
async createCollection(name: string, schema?: any): Promise<void> {
|
|
3739
|
+
await this.sendCRUD("CreateCollection", {
|
|
3740
|
+
name,
|
|
3741
|
+
schema: schema ?? {},
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
/** List all collections via WebSocket. */
|
|
3746
|
+
async listCollections(): Promise<string[]> {
|
|
3747
|
+
const data = await this.sendCRUD("GetCollections", {});
|
|
3748
|
+
return Array.isArray(data) ? data : [];
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
/** Delete a collection via WebSocket. */
|
|
3752
|
+
async deleteCollection(name: string): Promise<void> {
|
|
3753
|
+
await this.sendCRUD("DeleteCollection", { name });
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3385
3756
|
/**
|
|
3386
3757
|
* Close the WebSocket connection.
|
|
3387
3758
|
*/
|