@ekodb/ekodb-client 0.15.0 → 0.15.2
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 +67 -0
- package/dist/client.js +239 -2
- package/dist/client.test.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
- package/src/client.test.ts +1 -0
- package/src/client.ts +326 -1
- 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
|
@@ -1219,11 +1219,40 @@ export declare class EventStream<_T = unknown> {
|
|
|
1219
1219
|
/**
|
|
1220
1220
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1221
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;
|
|
1222
1250
|
export declare class WebSocketClient {
|
|
1223
1251
|
private wsURL;
|
|
1224
1252
|
private token;
|
|
1225
1253
|
private ws;
|
|
1226
1254
|
private dispatcherRunning;
|
|
1255
|
+
private schemaCache;
|
|
1227
1256
|
private pendingRequests;
|
|
1228
1257
|
private subscriptions;
|
|
1229
1258
|
private chatStreams;
|
|
@@ -1269,6 +1298,44 @@ export declare class WebSocketClient {
|
|
|
1269
1298
|
* proxy timeouts.
|
|
1270
1299
|
*/
|
|
1271
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>;
|
|
1272
1339
|
/**
|
|
1273
1340
|
* Close the WebSocket connection.
|
|
1274
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");
|
|
@@ -1719,7 +1720,10 @@ class EkoDBClient {
|
|
|
1719
1720
|
limit,
|
|
1720
1721
|
};
|
|
1721
1722
|
const response = await this.search(collection, searchQuery);
|
|
1722
|
-
return response.results.map((r) =>
|
|
1723
|
+
return response.results.map((r) => ({
|
|
1724
|
+
...r.record,
|
|
1725
|
+
_score: r.score,
|
|
1726
|
+
}));
|
|
1723
1727
|
}
|
|
1724
1728
|
/**
|
|
1725
1729
|
* Find all records in a collection with a limit
|
|
@@ -1778,10 +1782,107 @@ exports.EventStream = EventStream;
|
|
|
1778
1782
|
/**
|
|
1779
1783
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
1780
1784
|
*/
|
|
1785
|
+
/**
|
|
1786
|
+
* In-memory schema cache with TTL for primary_key_alias resolution.
|
|
1787
|
+
*/
|
|
1788
|
+
class SchemaCache {
|
|
1789
|
+
constructor(options = {}) {
|
|
1790
|
+
this.entries = new Map();
|
|
1791
|
+
this.lruOrder = [];
|
|
1792
|
+
this.enabled = options.enabled ?? false;
|
|
1793
|
+
this.maxEntries = options.maxEntries ?? 100;
|
|
1794
|
+
this.ttlMs = (options.ttlSeconds ?? 300) * 1000;
|
|
1795
|
+
}
|
|
1796
|
+
isEnabled() {
|
|
1797
|
+
return this.enabled;
|
|
1798
|
+
}
|
|
1799
|
+
setEnabled(enabled) {
|
|
1800
|
+
this.enabled = enabled;
|
|
1801
|
+
}
|
|
1802
|
+
getAlias(collection) {
|
|
1803
|
+
if (!this.enabled)
|
|
1804
|
+
return undefined;
|
|
1805
|
+
const entry = this.entries.get(collection);
|
|
1806
|
+
if (!entry)
|
|
1807
|
+
return undefined;
|
|
1808
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
1809
|
+
this.entries.delete(collection);
|
|
1810
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1811
|
+
return undefined;
|
|
1812
|
+
}
|
|
1813
|
+
this.touchLRU(collection);
|
|
1814
|
+
return entry.primaryKeyAlias;
|
|
1815
|
+
}
|
|
1816
|
+
insert(collection, primaryKeyAlias, version) {
|
|
1817
|
+
if (!this.enabled)
|
|
1818
|
+
return;
|
|
1819
|
+
const isNew = !this.entries.has(collection);
|
|
1820
|
+
this.entries.set(collection, {
|
|
1821
|
+
primaryKeyAlias,
|
|
1822
|
+
version,
|
|
1823
|
+
cachedAt: Date.now(),
|
|
1824
|
+
});
|
|
1825
|
+
if (isNew) {
|
|
1826
|
+
this.lruOrder.push(collection);
|
|
1827
|
+
while (this.lruOrder.length > this.maxEntries) {
|
|
1828
|
+
const oldest = this.lruOrder.shift();
|
|
1829
|
+
this.entries.delete(oldest);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
this.touchLRU(collection);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
invalidate(collection) {
|
|
1837
|
+
this.entries.delete(collection);
|
|
1838
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1839
|
+
}
|
|
1840
|
+
invalidateAll() {
|
|
1841
|
+
this.entries.clear();
|
|
1842
|
+
this.lruOrder = [];
|
|
1843
|
+
}
|
|
1844
|
+
handleSchemaChanged(collection, version, primaryKeyAlias) {
|
|
1845
|
+
if (!this.enabled)
|
|
1846
|
+
return;
|
|
1847
|
+
const existing = this.entries.get(collection);
|
|
1848
|
+
if (existing && version <= existing.version)
|
|
1849
|
+
return;
|
|
1850
|
+
this.insert(collection, primaryKeyAlias, version);
|
|
1851
|
+
}
|
|
1852
|
+
get size() {
|
|
1853
|
+
return this.entries.size;
|
|
1854
|
+
}
|
|
1855
|
+
touchLRU(collection) {
|
|
1856
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
1857
|
+
this.lruOrder.push(collection);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
exports.SchemaCache = SchemaCache;
|
|
1861
|
+
/**
|
|
1862
|
+
* Extract record ID from a record object, trying custom alias, "id", then "_id".
|
|
1863
|
+
*/
|
|
1864
|
+
function extractRecordId(record, extraCandidates = []) {
|
|
1865
|
+
for (const key of extraCandidates) {
|
|
1866
|
+
const val = record[key];
|
|
1867
|
+
if (typeof val === "string")
|
|
1868
|
+
return val;
|
|
1869
|
+
if (val && typeof val === "object" && "value" in val)
|
|
1870
|
+
return String(val.value);
|
|
1871
|
+
}
|
|
1872
|
+
for (const key of ["id", "_id"]) {
|
|
1873
|
+
const val = record[key];
|
|
1874
|
+
if (typeof val === "string")
|
|
1875
|
+
return val;
|
|
1876
|
+
if (val && typeof val === "object" && "value" in val)
|
|
1877
|
+
return String(val.value);
|
|
1878
|
+
}
|
|
1879
|
+
return undefined;
|
|
1880
|
+
}
|
|
1781
1881
|
class WebSocketClient {
|
|
1782
1882
|
constructor(wsURL, token) {
|
|
1783
1883
|
this.ws = null;
|
|
1784
1884
|
this.dispatcherRunning = false;
|
|
1885
|
+
this.schemaCache = null;
|
|
1785
1886
|
// Dispatcher state
|
|
1786
1887
|
this.pendingRequests = new Map();
|
|
1787
1888
|
this.subscriptions = new Map();
|
|
@@ -1956,6 +2057,12 @@ class WebSocketClient {
|
|
|
1956
2057
|
}
|
|
1957
2058
|
break;
|
|
1958
2059
|
}
|
|
2060
|
+
case "SchemaChanged": {
|
|
2061
|
+
if (this.schemaCache && msg.payload) {
|
|
2062
|
+
this.schemaCache.handleSchemaChanged(msg.payload.collection, msg.payload.version, msg.payload.primary_key_alias);
|
|
2063
|
+
}
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
1959
2066
|
case "ClientToolCall": {
|
|
1960
2067
|
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
1961
2068
|
const stream = this.chatStreams.get(chatId);
|
|
@@ -2120,6 +2227,136 @@ class WebSocketClient {
|
|
|
2120
2227
|
});
|
|
2121
2228
|
return { content: payload?.data?.content || "" };
|
|
2122
2229
|
}
|
|
2230
|
+
/** Attach a schema cache for automatic invalidation on SchemaChanged events. */
|
|
2231
|
+
setSchemaCache(cache) {
|
|
2232
|
+
this.schemaCache = cache;
|
|
2233
|
+
}
|
|
2234
|
+
/** Extract record ID using the schema cache's primary_key_alias. */
|
|
2235
|
+
extractId(collection, record) {
|
|
2236
|
+
const alias = this.schemaCache?.getAlias(collection);
|
|
2237
|
+
return extractRecordId(record, alias ? [alias] : []);
|
|
2238
|
+
}
|
|
2239
|
+
// =========================================================================
|
|
2240
|
+
// WS CRUD Methods — Full Parity with Server
|
|
2241
|
+
// =========================================================================
|
|
2242
|
+
async sendCRUD(msgType, payload) {
|
|
2243
|
+
const messageId = this.genMessageId();
|
|
2244
|
+
const response = await this.sendRequest({
|
|
2245
|
+
type: msgType,
|
|
2246
|
+
messageId,
|
|
2247
|
+
payload,
|
|
2248
|
+
});
|
|
2249
|
+
return response?.data ?? response;
|
|
2250
|
+
}
|
|
2251
|
+
/** Insert a single record via WebSocket. */
|
|
2252
|
+
async insert(collection, record, bypassRipple) {
|
|
2253
|
+
return this.sendCRUD("Insert", {
|
|
2254
|
+
collection,
|
|
2255
|
+
record,
|
|
2256
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
/** Query records via WebSocket. */
|
|
2260
|
+
async query(collection, options) {
|
|
2261
|
+
const data = await this.sendCRUD("Query", {
|
|
2262
|
+
collection,
|
|
2263
|
+
...options,
|
|
2264
|
+
});
|
|
2265
|
+
return Array.isArray(data) ? data : [];
|
|
2266
|
+
}
|
|
2267
|
+
/** Find a record by ID via WebSocket. */
|
|
2268
|
+
async findById(collection, id) {
|
|
2269
|
+
return this.sendCRUD("FindById", { collection, id });
|
|
2270
|
+
}
|
|
2271
|
+
/** Update a record by ID via WebSocket. */
|
|
2272
|
+
async update(collection, id, record, bypassRipple) {
|
|
2273
|
+
return this.sendCRUD("Update", {
|
|
2274
|
+
collection,
|
|
2275
|
+
id,
|
|
2276
|
+
record,
|
|
2277
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
/** Delete a record by ID via WebSocket. */
|
|
2281
|
+
async delete(collection, id, bypassRipple) {
|
|
2282
|
+
await this.sendCRUD("Delete", {
|
|
2283
|
+
collection,
|
|
2284
|
+
id,
|
|
2285
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
/** Batch insert multiple records via WebSocket. */
|
|
2289
|
+
async batchInsert(collection, records, bypassRipple) {
|
|
2290
|
+
return this.sendCRUD("BatchInsert", {
|
|
2291
|
+
collection,
|
|
2292
|
+
records,
|
|
2293
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
/** Batch update multiple records via WebSocket. */
|
|
2297
|
+
async batchUpdate(collection, updates, bypassRipple) {
|
|
2298
|
+
return this.sendCRUD("BatchUpdate", {
|
|
2299
|
+
collection,
|
|
2300
|
+
updates,
|
|
2301
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
/** Batch delete records by IDs via WebSocket. */
|
|
2305
|
+
async batchDelete(collection, ids, bypassRipple) {
|
|
2306
|
+
await this.sendCRUD("BatchDelete", {
|
|
2307
|
+
collection,
|
|
2308
|
+
ids,
|
|
2309
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
/** Full-text search via WebSocket. */
|
|
2313
|
+
async textSearch(collection, query, fields, limit) {
|
|
2314
|
+
const options = {};
|
|
2315
|
+
if (fields)
|
|
2316
|
+
options.fields = fields;
|
|
2317
|
+
if (limit)
|
|
2318
|
+
options.limit = limit;
|
|
2319
|
+
const data = await this.sendCRUD("TextSearch", {
|
|
2320
|
+
collection,
|
|
2321
|
+
query,
|
|
2322
|
+
...(Object.keys(options).length > 0 && { options }),
|
|
2323
|
+
});
|
|
2324
|
+
return Array.isArray(data) ? data : [];
|
|
2325
|
+
}
|
|
2326
|
+
/** Get distinct values for a field via WebSocket. */
|
|
2327
|
+
async distinctValues(collection, field, filter) {
|
|
2328
|
+
return this.sendCRUD("DistinctValues", {
|
|
2329
|
+
collection,
|
|
2330
|
+
field,
|
|
2331
|
+
...(filter && { filter }),
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
/** Apply an atomic field action via WebSocket. */
|
|
2335
|
+
async updateWithAction(collection, id, action, field, value) {
|
|
2336
|
+
return this.sendCRUD("UpdateWithAction", {
|
|
2337
|
+
collection,
|
|
2338
|
+
id,
|
|
2339
|
+
action,
|
|
2340
|
+
field,
|
|
2341
|
+
...(value !== undefined && { value }),
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
/** Create a collection with optional schema via WebSocket. */
|
|
2345
|
+
async createCollection(name, schema) {
|
|
2346
|
+
await this.sendCRUD("CreateCollection", {
|
|
2347
|
+
name,
|
|
2348
|
+
schema: schema ?? {},
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
/** List all collections via WebSocket. */
|
|
2352
|
+
async listCollections() {
|
|
2353
|
+
const data = await this.sendCRUD("GetCollections", {});
|
|
2354
|
+
return Array.isArray(data) ? data : [];
|
|
2355
|
+
}
|
|
2356
|
+
/** Delete a collection via WebSocket. */
|
|
2357
|
+
async deleteCollection(name) {
|
|
2358
|
+
await this.sendCRUD("DeleteCollection", { name });
|
|
2359
|
+
}
|
|
2123
2360
|
/**
|
|
2124
2361
|
* Close the WebSocket connection.
|
|
2125
2362
|
*/
|
package/dist/client.test.js
CHANGED
|
@@ -1944,6 +1944,7 @@ function mockErrorResponse(status, message) {
|
|
|
1944
1944
|
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
1945
1945
|
(0, vitest_1.expect)(result[0]).toHaveProperty("id", "doc_1");
|
|
1946
1946
|
(0, vitest_1.expect)(result[0]).toHaveProperty("title", "ML Guide");
|
|
1947
|
+
(0, vitest_1.expect)(result[0]).toHaveProperty("_score", 0.95);
|
|
1947
1948
|
});
|
|
1948
1949
|
});
|
|
1949
1950
|
// ============================================================================
|
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
|
@@ -2584,6 +2584,7 @@ describe("EkoDBClient text and hybrid search", () => {
|
|
|
2584
2584
|
expect(result).toHaveLength(1);
|
|
2585
2585
|
expect(result[0]).toHaveProperty("id", "doc_1");
|
|
2586
2586
|
expect(result[0]).toHaveProperty("title", "ML Guide");
|
|
2587
|
+
expect(result[0]).toHaveProperty("_score", 0.95);
|
|
2587
2588
|
});
|
|
2588
2589
|
});
|
|
2589
2590
|
|
package/src/client.ts
CHANGED
|
@@ -2912,7 +2912,10 @@ export class EkoDBClient {
|
|
|
2912
2912
|
};
|
|
2913
2913
|
|
|
2914
2914
|
const response = await this.search(collection, searchQuery);
|
|
2915
|
-
return response.results.map((r) =>
|
|
2915
|
+
return response.results.map((r) => ({
|
|
2916
|
+
...r.record,
|
|
2917
|
+
_score: r.score,
|
|
2918
|
+
}));
|
|
2916
2919
|
}
|
|
2917
2920
|
|
|
2918
2921
|
/**
|
|
@@ -3026,11 +3029,128 @@ export class EventStream<_T = unknown> {
|
|
|
3026
3029
|
/**
|
|
3027
3030
|
* WebSocket client for real-time queries, subscriptions, and chat streaming.
|
|
3028
3031
|
*/
|
|
3032
|
+
/**
|
|
3033
|
+
* In-memory schema cache with TTL for primary_key_alias resolution.
|
|
3034
|
+
*/
|
|
3035
|
+
export class SchemaCache {
|
|
3036
|
+
private entries: Map<
|
|
3037
|
+
string,
|
|
3038
|
+
{ primaryKeyAlias: string; version: number; cachedAt: number }
|
|
3039
|
+
> = new Map();
|
|
3040
|
+
private lruOrder: string[] = [];
|
|
3041
|
+
private enabled: boolean;
|
|
3042
|
+
private maxEntries: number;
|
|
3043
|
+
private ttlMs: number;
|
|
3044
|
+
|
|
3045
|
+
constructor(
|
|
3046
|
+
options: {
|
|
3047
|
+
enabled?: boolean;
|
|
3048
|
+
maxEntries?: number;
|
|
3049
|
+
ttlSeconds?: number;
|
|
3050
|
+
} = {},
|
|
3051
|
+
) {
|
|
3052
|
+
this.enabled = options.enabled ?? false;
|
|
3053
|
+
this.maxEntries = options.maxEntries ?? 100;
|
|
3054
|
+
this.ttlMs = (options.ttlSeconds ?? 300) * 1000;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
isEnabled(): boolean {
|
|
3058
|
+
return this.enabled;
|
|
3059
|
+
}
|
|
3060
|
+
setEnabled(enabled: boolean): void {
|
|
3061
|
+
this.enabled = enabled;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
getAlias(collection: string): string | undefined {
|
|
3065
|
+
if (!this.enabled) return undefined;
|
|
3066
|
+
const entry = this.entries.get(collection);
|
|
3067
|
+
if (!entry) return undefined;
|
|
3068
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
3069
|
+
this.entries.delete(collection);
|
|
3070
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3071
|
+
return undefined;
|
|
3072
|
+
}
|
|
3073
|
+
this.touchLRU(collection);
|
|
3074
|
+
return entry.primaryKeyAlias;
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
insert(collection: string, primaryKeyAlias: string, version: number): void {
|
|
3078
|
+
if (!this.enabled) return;
|
|
3079
|
+
const isNew = !this.entries.has(collection);
|
|
3080
|
+
this.entries.set(collection, {
|
|
3081
|
+
primaryKeyAlias,
|
|
3082
|
+
version,
|
|
3083
|
+
cachedAt: Date.now(),
|
|
3084
|
+
});
|
|
3085
|
+
if (isNew) {
|
|
3086
|
+
this.lruOrder.push(collection);
|
|
3087
|
+
while (this.lruOrder.length > this.maxEntries) {
|
|
3088
|
+
const oldest = this.lruOrder.shift()!;
|
|
3089
|
+
this.entries.delete(oldest);
|
|
3090
|
+
}
|
|
3091
|
+
} else {
|
|
3092
|
+
this.touchLRU(collection);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
invalidate(collection: string): void {
|
|
3097
|
+
this.entries.delete(collection);
|
|
3098
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3099
|
+
}
|
|
3100
|
+
invalidateAll(): void {
|
|
3101
|
+
this.entries.clear();
|
|
3102
|
+
this.lruOrder = [];
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
handleSchemaChanged(
|
|
3106
|
+
collection: string,
|
|
3107
|
+
version: number,
|
|
3108
|
+
primaryKeyAlias: string,
|
|
3109
|
+
): void {
|
|
3110
|
+
if (!this.enabled) return;
|
|
3111
|
+
const existing = this.entries.get(collection);
|
|
3112
|
+
if (existing && version <= existing.version) return;
|
|
3113
|
+
this.insert(collection, primaryKeyAlias, version);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
get size(): number {
|
|
3117
|
+
return this.entries.size;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
private touchLRU(collection: string): void {
|
|
3121
|
+
this.lruOrder = this.lruOrder.filter((c) => c !== collection);
|
|
3122
|
+
this.lruOrder.push(collection);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
/**
|
|
3127
|
+
* Extract record ID from a record object, trying custom alias, "id", then "_id".
|
|
3128
|
+
*/
|
|
3129
|
+
export function extractRecordId(
|
|
3130
|
+
record: Record,
|
|
3131
|
+
extraCandidates: string[] = [],
|
|
3132
|
+
): string | undefined {
|
|
3133
|
+
for (const key of extraCandidates) {
|
|
3134
|
+
const val = record[key];
|
|
3135
|
+
if (typeof val === "string") return val;
|
|
3136
|
+
if (val && typeof val === "object" && "value" in val)
|
|
3137
|
+
return String(val.value);
|
|
3138
|
+
}
|
|
3139
|
+
for (const key of ["id", "_id"]) {
|
|
3140
|
+
const val = record[key];
|
|
3141
|
+
if (typeof val === "string") return val;
|
|
3142
|
+
if (val && typeof val === "object" && "value" in val)
|
|
3143
|
+
return String(val.value);
|
|
3144
|
+
}
|
|
3145
|
+
return undefined;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3029
3148
|
export class WebSocketClient {
|
|
3030
3149
|
private wsURL: string;
|
|
3031
3150
|
private token: string;
|
|
3032
3151
|
private ws: any = null;
|
|
3033
3152
|
private dispatcherRunning = false;
|
|
3153
|
+
private schemaCache: SchemaCache | null = null;
|
|
3034
3154
|
|
|
3035
3155
|
// Dispatcher state
|
|
3036
3156
|
private pendingRequests: Map<
|
|
@@ -3230,6 +3350,17 @@ export class WebSocketClient {
|
|
|
3230
3350
|
break;
|
|
3231
3351
|
}
|
|
3232
3352
|
|
|
3353
|
+
case "SchemaChanged": {
|
|
3354
|
+
if (this.schemaCache && msg.payload) {
|
|
3355
|
+
this.schemaCache.handleSchemaChanged(
|
|
3356
|
+
msg.payload.collection,
|
|
3357
|
+
msg.payload.version,
|
|
3358
|
+
msg.payload.primary_key_alias,
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
break;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3233
3364
|
case "ClientToolCall": {
|
|
3234
3365
|
const chatId = msg.payload?.chat_id || msg.payload?.chatId;
|
|
3235
3366
|
const stream = this.chatStreams.get(chatId);
|
|
@@ -3431,6 +3562,200 @@ export class WebSocketClient {
|
|
|
3431
3562
|
return { content: payload?.data?.content || "" };
|
|
3432
3563
|
}
|
|
3433
3564
|
|
|
3565
|
+
/** Attach a schema cache for automatic invalidation on SchemaChanged events. */
|
|
3566
|
+
setSchemaCache(cache: SchemaCache): void {
|
|
3567
|
+
this.schemaCache = cache;
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3570
|
+
/** Extract record ID using the schema cache's primary_key_alias. */
|
|
3571
|
+
extractId(collection: string, record: Record): string | undefined {
|
|
3572
|
+
const alias = this.schemaCache?.getAlias(collection);
|
|
3573
|
+
return extractRecordId(record, alias ? [alias] : []);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// =========================================================================
|
|
3577
|
+
// WS CRUD Methods — Full Parity with Server
|
|
3578
|
+
// =========================================================================
|
|
3579
|
+
|
|
3580
|
+
private async sendCRUD(msgType: string, payload: any): Promise<any> {
|
|
3581
|
+
const messageId = this.genMessageId();
|
|
3582
|
+
const response = await this.sendRequest({
|
|
3583
|
+
type: msgType,
|
|
3584
|
+
messageId,
|
|
3585
|
+
payload,
|
|
3586
|
+
});
|
|
3587
|
+
return response?.data ?? response;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
/** Insert a single record via WebSocket. */
|
|
3591
|
+
async insert(
|
|
3592
|
+
collection: string,
|
|
3593
|
+
record: Record,
|
|
3594
|
+
bypassRipple?: boolean,
|
|
3595
|
+
): Promise<any> {
|
|
3596
|
+
return this.sendCRUD("Insert", {
|
|
3597
|
+
collection,
|
|
3598
|
+
record,
|
|
3599
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
/** Query records via WebSocket. */
|
|
3604
|
+
async query(
|
|
3605
|
+
collection: string,
|
|
3606
|
+
options?: {
|
|
3607
|
+
filter?: any;
|
|
3608
|
+
sort?: any;
|
|
3609
|
+
limit?: number;
|
|
3610
|
+
skip?: number;
|
|
3611
|
+
},
|
|
3612
|
+
): Promise<any[]> {
|
|
3613
|
+
const data = await this.sendCRUD("Query", {
|
|
3614
|
+
collection,
|
|
3615
|
+
...options,
|
|
3616
|
+
});
|
|
3617
|
+
return Array.isArray(data) ? data : [];
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
/** Find a record by ID via WebSocket. */
|
|
3621
|
+
async findById(collection: string, id: string): Promise<any> {
|
|
3622
|
+
return this.sendCRUD("FindById", { collection, id });
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
/** Update a record by ID via WebSocket. */
|
|
3626
|
+
async update(
|
|
3627
|
+
collection: string,
|
|
3628
|
+
id: string,
|
|
3629
|
+
record: Record,
|
|
3630
|
+
bypassRipple?: boolean,
|
|
3631
|
+
): Promise<any> {
|
|
3632
|
+
return this.sendCRUD("Update", {
|
|
3633
|
+
collection,
|
|
3634
|
+
id,
|
|
3635
|
+
record,
|
|
3636
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
/** Delete a record by ID via WebSocket. */
|
|
3641
|
+
async delete(
|
|
3642
|
+
collection: string,
|
|
3643
|
+
id: string,
|
|
3644
|
+
bypassRipple?: boolean,
|
|
3645
|
+
): Promise<void> {
|
|
3646
|
+
await this.sendCRUD("Delete", {
|
|
3647
|
+
collection,
|
|
3648
|
+
id,
|
|
3649
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
/** Batch insert multiple records via WebSocket. */
|
|
3654
|
+
async batchInsert(
|
|
3655
|
+
collection: string,
|
|
3656
|
+
records: Record[],
|
|
3657
|
+
bypassRipple?: boolean,
|
|
3658
|
+
): Promise<any> {
|
|
3659
|
+
return this.sendCRUD("BatchInsert", {
|
|
3660
|
+
collection,
|
|
3661
|
+
records,
|
|
3662
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
/** Batch update multiple records via WebSocket. */
|
|
3667
|
+
async batchUpdate(
|
|
3668
|
+
collection: string,
|
|
3669
|
+
updates: [string, Record][],
|
|
3670
|
+
bypassRipple?: boolean,
|
|
3671
|
+
): Promise<any> {
|
|
3672
|
+
return this.sendCRUD("BatchUpdate", {
|
|
3673
|
+
collection,
|
|
3674
|
+
updates,
|
|
3675
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
/** Batch delete records by IDs via WebSocket. */
|
|
3680
|
+
async batchDelete(
|
|
3681
|
+
collection: string,
|
|
3682
|
+
ids: string[],
|
|
3683
|
+
bypassRipple?: boolean,
|
|
3684
|
+
): Promise<void> {
|
|
3685
|
+
await this.sendCRUD("BatchDelete", {
|
|
3686
|
+
collection,
|
|
3687
|
+
ids,
|
|
3688
|
+
...(bypassRipple !== undefined && { bypass_ripple: bypassRipple }),
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/** Full-text search via WebSocket. */
|
|
3693
|
+
async textSearch(
|
|
3694
|
+
collection: string,
|
|
3695
|
+
query: string,
|
|
3696
|
+
fields?: string[],
|
|
3697
|
+
limit?: number,
|
|
3698
|
+
): Promise<any[]> {
|
|
3699
|
+
const options: any = {};
|
|
3700
|
+
if (fields) options.fields = fields;
|
|
3701
|
+
if (limit) options.limit = limit;
|
|
3702
|
+
const data = await this.sendCRUD("TextSearch", {
|
|
3703
|
+
collection,
|
|
3704
|
+
query,
|
|
3705
|
+
...(Object.keys(options).length > 0 && { options }),
|
|
3706
|
+
});
|
|
3707
|
+
return Array.isArray(data) ? data : [];
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
/** Get distinct values for a field via WebSocket. */
|
|
3711
|
+
async distinctValues(
|
|
3712
|
+
collection: string,
|
|
3713
|
+
field: string,
|
|
3714
|
+
filter?: any,
|
|
3715
|
+
): Promise<any> {
|
|
3716
|
+
return this.sendCRUD("DistinctValues", {
|
|
3717
|
+
collection,
|
|
3718
|
+
field,
|
|
3719
|
+
...(filter && { filter }),
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
/** Apply an atomic field action via WebSocket. */
|
|
3724
|
+
async updateWithAction(
|
|
3725
|
+
collection: string,
|
|
3726
|
+
id: string,
|
|
3727
|
+
action: string,
|
|
3728
|
+
field: string,
|
|
3729
|
+
value?: any,
|
|
3730
|
+
): Promise<any> {
|
|
3731
|
+
return this.sendCRUD("UpdateWithAction", {
|
|
3732
|
+
collection,
|
|
3733
|
+
id,
|
|
3734
|
+
action,
|
|
3735
|
+
field,
|
|
3736
|
+
...(value !== undefined && { value }),
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
/** Create a collection with optional schema via WebSocket. */
|
|
3741
|
+
async createCollection(name: string, schema?: any): Promise<void> {
|
|
3742
|
+
await this.sendCRUD("CreateCollection", {
|
|
3743
|
+
name,
|
|
3744
|
+
schema: schema ?? {},
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
/** List all collections via WebSocket. */
|
|
3749
|
+
async listCollections(): Promise<string[]> {
|
|
3750
|
+
const data = await this.sendCRUD("GetCollections", {});
|
|
3751
|
+
return Array.isArray(data) ? data : [];
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
/** Delete a collection via WebSocket. */
|
|
3755
|
+
async deleteCollection(name: string): Promise<void> {
|
|
3756
|
+
await this.sendCRUD("DeleteCollection", { name });
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3434
3759
|
/**
|
|
3435
3760
|
* Close the WebSocket connection.
|
|
3436
3761
|
*/
|