@koolbase/react-native 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1025 -0
- package/dist/analytics.d.ts +26 -0
- package/dist/analytics.js +138 -0
- package/dist/apple-auth.d.ts +22 -0
- package/dist/apple-auth.js +74 -0
- package/dist/auth-errors.d.ts +117 -0
- package/dist/auth-errors.js +250 -0
- package/dist/auth-storage.d.ts +26 -0
- package/dist/auth-storage.js +105 -0
- package/dist/auth.d.ts +199 -0
- package/dist/auth.js +794 -0
- package/dist/cache-store.d.ts +11 -0
- package/dist/cache-store.js +136 -0
- package/dist/code-push.d.ts +59 -0
- package/dist/code-push.js +255 -0
- package/dist/database-errors.d.ts +95 -0
- package/dist/database-errors.js +173 -0
- package/dist/database.d.ts +208 -0
- package/dist/database.js +508 -0
- package/dist/device-metadata.d.ts +36 -0
- package/dist/device-metadata.js +102 -0
- package/dist/flags.d.ts +15 -0
- package/dist/flags.js +76 -0
- package/dist/functions.d.ts +8 -0
- package/dist/functions.js +70 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +194 -0
- package/dist/logic-engine.d.ts +17 -0
- package/dist/logic-engine.js +193 -0
- package/dist/messaging.d.ts +20 -0
- package/dist/messaging.js +58 -0
- package/dist/realtime.d.ts +19 -0
- package/dist/realtime.js +148 -0
- package/dist/record.d.ts +2 -0
- package/dist/record.js +20 -0
- package/dist/storage-errors.d.ts +163 -0
- package/dist/storage-errors.js +249 -0
- package/dist/storage.d.ts +184 -0
- package/dist/storage.js +438 -0
- package/dist/sync-engine.d.ts +16 -0
- package/dist/sync-engine.js +86 -0
- package/dist/types.d.ts +470 -0
- package/dist/types.js +40 -0
- package/package.json +52 -0
package/dist/database.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KoolbaseDatabase = void 0;
|
|
4
|
+
const cache_store_1 = require("./cache-store");
|
|
5
|
+
const sync_engine_1 = require("./sync-engine");
|
|
6
|
+
const record_1 = require("./record");
|
|
7
|
+
const database_errors_1 = require("./database-errors");
|
|
8
|
+
function generateId() {
|
|
9
|
+
return 'local_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
10
|
+
}
|
|
11
|
+
function batchOpToWire(op) {
|
|
12
|
+
switch (op.type) {
|
|
13
|
+
case 'insert':
|
|
14
|
+
return { type: 'insert', collection: op.collection, data: op.data };
|
|
15
|
+
case 'update':
|
|
16
|
+
return { type: 'update', record_id: op.recordId, data: op.data };
|
|
17
|
+
case 'delete':
|
|
18
|
+
return { type: 'delete', record_id: op.recordId };
|
|
19
|
+
case 'upsert':
|
|
20
|
+
return {
|
|
21
|
+
type: 'upsert',
|
|
22
|
+
collection: op.collection,
|
|
23
|
+
match: op.match,
|
|
24
|
+
data: op.data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
class KoolbaseDatabase {
|
|
29
|
+
constructor(config, getUserId, getToken) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.getUserId = getUserId;
|
|
32
|
+
this.getToken = getToken;
|
|
33
|
+
this.syncEngine = new sync_engine_1.SyncEngine(config, getUserId, getToken);
|
|
34
|
+
this.syncEngine.start();
|
|
35
|
+
}
|
|
36
|
+
// getUserId is kept only for local cache keys / offline metadata; request
|
|
37
|
+
// identity now comes solely from the verified access token.
|
|
38
|
+
async buildHeaders() {
|
|
39
|
+
const token = await this.getToken();
|
|
40
|
+
return {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'x-api-key': this.config.publicKey,
|
|
43
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async request(method, path, body) {
|
|
47
|
+
const res = await fetch(`${this.config.baseUrl}${path}`, {
|
|
48
|
+
method,
|
|
49
|
+
headers: await this.buildHeaders(),
|
|
50
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
51
|
+
});
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, data, `Request failed: ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
// ─── Query (cache-first) ───────────────────────────────────────────────────
|
|
59
|
+
async runQuery(collection, options) {
|
|
60
|
+
const raw = await this.request('POST', '/v1/sdk/db/query', {
|
|
61
|
+
collection,
|
|
62
|
+
filters: options.filters ?? {},
|
|
63
|
+
limit: options.limit ?? 20,
|
|
64
|
+
offset: options.offset ?? 0,
|
|
65
|
+
order_by: options.orderBy,
|
|
66
|
+
order_desc: options.orderDesc ?? false,
|
|
67
|
+
populate: options.populate ?? [],
|
|
68
|
+
});
|
|
69
|
+
return { records: raw.records.map(record_1.recordFromWire), total: raw.total };
|
|
70
|
+
}
|
|
71
|
+
async query(collection, options = {}) {
|
|
72
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
73
|
+
const queryHash = (0, cache_store_1.hashQuery)(collection, options);
|
|
74
|
+
const cached = await (0, cache_store_1.getCached)(userId, collection, queryHash);
|
|
75
|
+
this.runQuery(collection, options)
|
|
76
|
+
.then(result => (0, cache_store_1.setCached)(userId, collection, queryHash, result))
|
|
77
|
+
.catch(() => {
|
|
78
|
+
// Network unavailable — cached data already returned
|
|
79
|
+
});
|
|
80
|
+
if (cached) {
|
|
81
|
+
return { ...cached, isFromCache: true };
|
|
82
|
+
}
|
|
83
|
+
const result = await this.runQuery(collection, options);
|
|
84
|
+
await (0, cache_store_1.setCached)(userId, collection, queryHash, result);
|
|
85
|
+
return { ...result, isFromCache: false };
|
|
86
|
+
}
|
|
87
|
+
// ─── Insert (online-first with offline fallback) ───────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Insert a new record into a collection.
|
|
90
|
+
*
|
|
91
|
+
* Online-first: awaits the server so a server-side rejection (unique
|
|
92
|
+
* violation, validation error, permission denial) surfaces as the typed
|
|
93
|
+
* `KoolbaseDataError` subclass — `insert` now throws `KoolbaseConflictError`
|
|
94
|
+
* with the offending field on a 409, matching `upsert` and `update`.
|
|
95
|
+
*
|
|
96
|
+
* On genuine network failure (server unreachable, timeout) the write is
|
|
97
|
+
* accepted optimistically: saved to the local cache and queued for sync
|
|
98
|
+
* when connectivity returns.
|
|
99
|
+
*/
|
|
100
|
+
async insert(collection, data) {
|
|
101
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
102
|
+
try {
|
|
103
|
+
// Online path: await the server and return the authoritative record
|
|
104
|
+
// (with the server-assigned id). Refresh the collection cache so the
|
|
105
|
+
// next query sees real data instead of a stale optimistic copy.
|
|
106
|
+
const raw = await this.request('POST', '/v1/sdk/db/insert', { collection, data });
|
|
107
|
+
const record = (0, record_1.recordFromWire)(raw);
|
|
108
|
+
await (0, cache_store_1.invalidateCache)(userId, collection);
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// Server-reachable rejection: the server saw the request and refused.
|
|
113
|
+
// Surface to the caller without writing optimistic state or queuing —
|
|
114
|
+
// the server has already decided it will not accept this write, and
|
|
115
|
+
// queuing it would just spin SyncEngine until max retries.
|
|
116
|
+
if (e instanceof database_errors_1.KoolbaseDataError)
|
|
117
|
+
throw e;
|
|
118
|
+
// Genuine network failure → offline path: save to local cache and
|
|
119
|
+
// queue for SyncEngine to retry when online. Return the optimistic
|
|
120
|
+
// record so the UI has something to render in the meantime.
|
|
121
|
+
const optimisticRecord = {
|
|
122
|
+
id: generateId(),
|
|
123
|
+
createdBy: userId,
|
|
124
|
+
data,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
updatedAt: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
await (0, cache_store_1.optimisticallyInsert)(userId, collection, optimisticRecord);
|
|
129
|
+
await (0, cache_store_1.addToWriteQueue)(userId, {
|
|
130
|
+
id: generateId(),
|
|
131
|
+
type: 'insert',
|
|
132
|
+
collection,
|
|
133
|
+
data,
|
|
134
|
+
});
|
|
135
|
+
return optimisticRecord;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ─── Upsert (online-only) ─────────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Insert a record, or update the existing one matching `match`.
|
|
141
|
+
*
|
|
142
|
+
* The server decides: exactly one match updates it, no match inserts a new
|
|
143
|
+
* record (seeded with the `match` fields), more than one match is an error.
|
|
144
|
+
* Returns the resulting record and a `created` flag (true = inserted, false
|
|
145
|
+
* = updated).
|
|
146
|
+
*
|
|
147
|
+
* Online-only by design. Unlike `insert`, an upsert is NOT queued offline:
|
|
148
|
+
* the insert-vs-update decision needs the server's authoritative view of
|
|
149
|
+
* what already exists, so deferring it could create a duplicate or apply a
|
|
150
|
+
* wrong update on later sync. It throws on network failure instead. A raw
|
|
151
|
+
* fetch is used (not `request`) so the status code is readable: 201 =
|
|
152
|
+
* created, 200 = updated.
|
|
153
|
+
*/
|
|
154
|
+
async upsert(collection, match, data) {
|
|
155
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/upsert`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: await this.buildHeaders(),
|
|
158
|
+
body: JSON.stringify({ collection, match, data }),
|
|
159
|
+
});
|
|
160
|
+
const body = await res.json();
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, body, `Upsert failed: ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
const created = res.status === 201;
|
|
165
|
+
const record = (0, record_1.recordFromWire)(body);
|
|
166
|
+
// Keep the cache fresh, same intent as insert's post-success invalidate.
|
|
167
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
168
|
+
await (0, cache_store_1.invalidateCache)(userId, collection);
|
|
169
|
+
return { record, created };
|
|
170
|
+
}
|
|
171
|
+
// ─── Delete where (online-only) ─────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* Bulk-delete every record in `collection` matching `filters`.
|
|
174
|
+
*
|
|
175
|
+
* The server applies the collection's delete rule (scoping to the caller for
|
|
176
|
+
* owner/scoped rules) and returns the number of records deleted.
|
|
177
|
+
*
|
|
178
|
+
* Online-only by design — like upsert, this is NOT queued offline: a bulk
|
|
179
|
+
* delete needs the server's authoritative view of what matches, so it throws
|
|
180
|
+
* on network failure rather than risk deleting the wrong set on later sync.
|
|
181
|
+
* The collection cache is invalidated on success.
|
|
182
|
+
*/
|
|
183
|
+
async deleteWhere(collection, filters) {
|
|
184
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/delete-where`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: await this.buildHeaders(),
|
|
187
|
+
body: JSON.stringify({ collection, filters }),
|
|
188
|
+
});
|
|
189
|
+
const body = await res.json();
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, body, `Delete failed: ${res.status}`);
|
|
192
|
+
}
|
|
193
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
194
|
+
await (0, cache_store_1.invalidateCache)(userId, collection);
|
|
195
|
+
return body.deleted ?? 0;
|
|
196
|
+
}
|
|
197
|
+
// ─── Batch (atomic, online-only) ────────────────────────────────────────────
|
|
198
|
+
/**
|
|
199
|
+
* Run multiple writes as a single atomic transaction.
|
|
200
|
+
*
|
|
201
|
+
* All `operations` commit together or none are applied — the server runs
|
|
202
|
+
* them in one database transaction and rolls back entirely on any failure.
|
|
203
|
+
* Operations apply in order and may span multiple collections.
|
|
204
|
+
*
|
|
205
|
+
* Online-only by design (like `upsert` and `deleteWhere`): atomicity needs
|
|
206
|
+
* the server's authoritative view, so a batch is never queued offline — it
|
|
207
|
+
* throws on network failure. A server-side rejection throws a
|
|
208
|
+
* `KoolbaseDataException` whose message identifies which operation failed;
|
|
209
|
+
* nothing was persisted.
|
|
210
|
+
*
|
|
211
|
+
* Returns one `BatchResult` per operation, in order.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* const results = await Koolbase.db.batch([
|
|
215
|
+
* BatchOp.insert('orders', { total: 50 }),
|
|
216
|
+
* BatchOp.update(inventoryId, { stock: 9 }),
|
|
217
|
+
* BatchOp.upsert('counters', { match: { name: 'orders' }, data: { value: 1 } }),
|
|
218
|
+
* BatchOp.delete(cartItemId),
|
|
219
|
+
* ]);
|
|
220
|
+
*/
|
|
221
|
+
async batch(operations) {
|
|
222
|
+
if (operations.length === 0) {
|
|
223
|
+
throw new Error('batch requires at least one operation');
|
|
224
|
+
}
|
|
225
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/batch`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
headers: await this.buildHeaders(),
|
|
228
|
+
body: JSON.stringify({
|
|
229
|
+
operations: operations.map(batchOpToWire),
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
const body = await res.json();
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, body, `Batch failed: ${res.status}`);
|
|
235
|
+
}
|
|
236
|
+
const results = (body.results ?? []).map(r => ({
|
|
237
|
+
type: r.type ?? '',
|
|
238
|
+
record: r.record
|
|
239
|
+
? (0, record_1.recordFromWire)(r.record)
|
|
240
|
+
: undefined,
|
|
241
|
+
created: r.created,
|
|
242
|
+
deleted: r.deleted ?? false,
|
|
243
|
+
}));
|
|
244
|
+
// Keep the cache consistent with what committed. Insert/upsert carry the
|
|
245
|
+
// collection in the input op; update/delete address records by id, so we
|
|
246
|
+
// don't know the collection at this layer — those refresh naturally on
|
|
247
|
+
// the next query for the affected collection.
|
|
248
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
249
|
+
const touched = new Set();
|
|
250
|
+
for (const op of operations) {
|
|
251
|
+
if (op.type === 'insert' || op.type === 'upsert') {
|
|
252
|
+
touched.add(op.collection);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const col of touched) {
|
|
256
|
+
await (0, cache_store_1.invalidateCache)(userId, col);
|
|
257
|
+
}
|
|
258
|
+
return results;
|
|
259
|
+
}
|
|
260
|
+
// ─── Get single record ──────────────────────────────────────────────────────
|
|
261
|
+
// ─── Get single record ──────────────────────────────────────────────────────
|
|
262
|
+
async get(recordId) {
|
|
263
|
+
const raw = await this.request('GET', `/v1/sdk/db/records/${recordId}`);
|
|
264
|
+
return (0, record_1.recordFromWire)(raw);
|
|
265
|
+
}
|
|
266
|
+
// ─── Update (online-first with offline fallback) ───────────────────────────
|
|
267
|
+
/**
|
|
268
|
+
* Update a record's fields by id.
|
|
269
|
+
*
|
|
270
|
+
* Online-first: awaits the server so a server-side rejection (unique
|
|
271
|
+
* violation, not found, permission denial) surfaces as the typed
|
|
272
|
+
* `KoolbaseDataError` subclass. An update that would violate a unique
|
|
273
|
+
* constraint now throws `KoolbaseConflictError` with the offending field —
|
|
274
|
+
* same shape as `insert` and `upsert`.
|
|
275
|
+
*
|
|
276
|
+
* On genuine network failure the update is queued for sync and a partial
|
|
277
|
+
* optimistic record is returned so the UI can re-render the new fields
|
|
278
|
+
* immediately.
|
|
279
|
+
*/
|
|
280
|
+
async update(recordId, data) {
|
|
281
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
282
|
+
try {
|
|
283
|
+
const raw = await this.request('PATCH', `/v1/sdk/db/records/${recordId}`, { data });
|
|
284
|
+
return (0, record_1.recordFromWire)(raw);
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
// Server-reachable rejection: surface to caller without queuing — the
|
|
288
|
+
// server already refused the write and will refuse it again on retry.
|
|
289
|
+
if (e instanceof database_errors_1.KoolbaseDataError)
|
|
290
|
+
throw e;
|
|
291
|
+
// Genuine network failure → queue for sync and return an optimistic
|
|
292
|
+
// partial record so the UI reflects the update immediately.
|
|
293
|
+
await (0, cache_store_1.addToWriteQueue)(userId, {
|
|
294
|
+
id: generateId(),
|
|
295
|
+
type: 'update',
|
|
296
|
+
recordId,
|
|
297
|
+
data,
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
id: recordId,
|
|
301
|
+
data,
|
|
302
|
+
createdAt: '',
|
|
303
|
+
updatedAt: new Date().toISOString(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ─── Delete ─────────────────────────────────────────────────────────────────
|
|
308
|
+
async delete(recordId) {
|
|
309
|
+
const userId = this.getUserId() ?? 'anonymous';
|
|
310
|
+
// Add to write queue
|
|
311
|
+
await (0, cache_store_1.addToWriteQueue)(userId, {
|
|
312
|
+
id: generateId(),
|
|
313
|
+
type: 'delete',
|
|
314
|
+
recordId,
|
|
315
|
+
});
|
|
316
|
+
// Try network
|
|
317
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/records/${recordId}`, { method: 'DELETE', headers: await this.buildHeaders(), });
|
|
318
|
+
if (!res.ok && res.status !== 204) {
|
|
319
|
+
// Queued for sync — will retry when online
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ─── Vectors ────────────────────────────────────────────────────────────────
|
|
323
|
+
/**
|
|
324
|
+
* Write (or replace) a vector for a record on the named `field`.
|
|
325
|
+
*
|
|
326
|
+
* The field must already be declared on the collection via the dashboard
|
|
327
|
+
* or CLI. `vector.length` must match the field's declared dimension;
|
|
328
|
+
* otherwise throws `KoolbaseVectorDimensionMismatchError`.
|
|
329
|
+
*
|
|
330
|
+
* Online-only — vectors are not cached locally or queued offline because
|
|
331
|
+
* HNSW similarity search has no useful offline semantics.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* await Koolbase.db.setVector(
|
|
335
|
+
* articleId,
|
|
336
|
+
* 'embedding',
|
|
337
|
+
* await myEmbeddingModel.encode(article.content),
|
|
338
|
+
* );
|
|
339
|
+
*/
|
|
340
|
+
async setVector(recordId, field, vector) {
|
|
341
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/set-vector`, {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: await this.buildHeaders(),
|
|
344
|
+
body: JSON.stringify({ record_id: recordId, field, vector }),
|
|
345
|
+
});
|
|
346
|
+
if (res.status !== 204) {
|
|
347
|
+
const body = await res.json().catch(() => ({}));
|
|
348
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, body, 'Set vector failed');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Read a record's stored vector on the named `field`.
|
|
353
|
+
*
|
|
354
|
+
* Throws `KoolbaseNotFoundError` if either the field is not declared or
|
|
355
|
+
* no vector has been set for this record on this field. Throws
|
|
356
|
+
* `KoolbasePermissionError` if the caller cannot read this record per
|
|
357
|
+
* the collection's read rule.
|
|
358
|
+
*
|
|
359
|
+
* Online-only.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* const v = await Koolbase.db.getVector(articleId, 'embedding');
|
|
363
|
+
* console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
|
|
364
|
+
*/
|
|
365
|
+
async getVector(recordId, field) {
|
|
366
|
+
const raw = await this.request('POST', '/v1/sdk/db/get-vector', { record_id: recordId, field });
|
|
367
|
+
return {
|
|
368
|
+
recordId: raw.record_id,
|
|
369
|
+
fieldName: raw.field_name,
|
|
370
|
+
vector: raw.vector,
|
|
371
|
+
createdAt: raw.created_at,
|
|
372
|
+
updatedAt: raw.updated_at,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Remove a record's stored vector on the named `field`.
|
|
377
|
+
*
|
|
378
|
+
* Online-only. Throws `KoolbaseNotFoundError` if no vector is set for
|
|
379
|
+
* `(recordId, field)`; throws `KoolbasePermissionError` if the caller
|
|
380
|
+
* cannot write this record per the collection's write rule.
|
|
381
|
+
*
|
|
382
|
+
* Note: this removes the vector from the dimension table but does NOT
|
|
383
|
+
* remove the field declaration itself — the field stays on the
|
|
384
|
+
* collection and is still settable on other records.
|
|
385
|
+
*/
|
|
386
|
+
async deleteVector(recordId, field) {
|
|
387
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/delete-vector`, {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: await this.buildHeaders(),
|
|
390
|
+
body: JSON.stringify({ record_id: recordId, field }),
|
|
391
|
+
});
|
|
392
|
+
if (res.status !== 204) {
|
|
393
|
+
const body = await res.json().catch(() => ({}));
|
|
394
|
+
throw (0, database_errors_1.koolbaseDataError)(res.status, body, 'Delete vector failed');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Queue an embedding job for a record's vector field. The server's
|
|
399
|
+
* embedding worker picks it up within ~1 second.
|
|
400
|
+
*
|
|
401
|
+
* If `text` is omitted, the vector field's configured `source_field`
|
|
402
|
+
* value on the record is used.
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* await Koolbase.db.embedText({
|
|
406
|
+
* collection: 'articles',
|
|
407
|
+
* recordId: article.$id,
|
|
408
|
+
* vectorField: 'content_embedding',
|
|
409
|
+
* });
|
|
410
|
+
*/
|
|
411
|
+
async embedText(opts) {
|
|
412
|
+
const body = {
|
|
413
|
+
collection: opts.collection,
|
|
414
|
+
record_id: opts.recordId,
|
|
415
|
+
vector_field: opts.vectorField,
|
|
416
|
+
};
|
|
417
|
+
if (opts.text && opts.text.length > 0) {
|
|
418
|
+
body.text = opts.text;
|
|
419
|
+
}
|
|
420
|
+
await this.request('POST', '/v1/sdk/db/embed-text', body);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Search for records based on their semantic similarity to a query.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* // Server-side embedding — most common:
|
|
427
|
+
* const result = await Koolbase.db.searchSemantic({
|
|
428
|
+
* collection: 'articles',
|
|
429
|
+
* field: 'content_embedding',
|
|
430
|
+
* queryText: 'how do I configure CI/CD?',
|
|
431
|
+
* limit: 10,
|
|
432
|
+
* });
|
|
433
|
+
*
|
|
434
|
+
* // Client-side embedding:
|
|
435
|
+
* const result = await Koolbase.db.searchSemantic({
|
|
436
|
+
* collection: 'articles',
|
|
437
|
+
* field: 'content_embedding',
|
|
438
|
+
* queryVector: precomputed,
|
|
439
|
+
* limit: 10,
|
|
440
|
+
* });
|
|
441
|
+
*
|
|
442
|
+
* // Hybrid search (vector + BM25, RRF-fused):
|
|
443
|
+
* const result = await Koolbase.db.searchSemantic({
|
|
444
|
+
* collection: 'articles',
|
|
445
|
+
* field: 'content_embedding',
|
|
446
|
+
* queryText: 'how do I configure CI/CD?',
|
|
447
|
+
* mode: 'hybrid',
|
|
448
|
+
* minSimilarity: 70,
|
|
449
|
+
* });
|
|
450
|
+
*
|
|
451
|
+
* `mode` selects the retrieval strategy:
|
|
452
|
+
* - `'semantic'` (default) — pure vector search via HNSW
|
|
453
|
+
* - `'lexical'` — pure BM25 over the field's source text
|
|
454
|
+
* - `'hybrid'` — vector + lexical, RRF-fused (k=60)
|
|
455
|
+
*
|
|
456
|
+
* `minSimilarity` (0..100, optional) filters out results below the
|
|
457
|
+
* given similarity percentage server-side. Saves bandwidth on weak
|
|
458
|
+
* matches. Only valid for semantic and hybrid; rejected by the
|
|
459
|
+
* server on lexical mode.
|
|
460
|
+
*/
|
|
461
|
+
async searchSemantic(opts) {
|
|
462
|
+
const hasVector = Array.isArray(opts.queryVector) && opts.queryVector.length > 0;
|
|
463
|
+
const hasText = typeof opts.queryText === 'string' && opts.queryText.trim().length > 0;
|
|
464
|
+
if (!hasVector && !hasText) {
|
|
465
|
+
throw new Error('searchSemantic: provide either queryVector or queryText.');
|
|
466
|
+
}
|
|
467
|
+
if (hasVector && hasText) {
|
|
468
|
+
throw new Error('searchSemantic: provide only one of queryVector or queryText.');
|
|
469
|
+
}
|
|
470
|
+
if (opts.minSimilarity !== undefined &&
|
|
471
|
+
(opts.minSimilarity < 0 || opts.minSimilarity > 100)) {
|
|
472
|
+
throw new Error(`searchSemantic: minSimilarity must be between 0 and 100, got ${opts.minSimilarity}.`);
|
|
473
|
+
}
|
|
474
|
+
const body = {
|
|
475
|
+
collection: opts.collection,
|
|
476
|
+
field: opts.field,
|
|
477
|
+
limit: opts.limit ?? 20,
|
|
478
|
+
// Always send mode so the server uses the SDK's intent rather
|
|
479
|
+
// than its own default. Omitting for 'semantic' would also work
|
|
480
|
+
// (server defaults to semantic) but explicit is safer if the
|
|
481
|
+
// server's default ever shifts.
|
|
482
|
+
mode: opts.mode ?? 'semantic',
|
|
483
|
+
};
|
|
484
|
+
if (hasVector)
|
|
485
|
+
body.query_vector = opts.queryVector;
|
|
486
|
+
if (hasText)
|
|
487
|
+
body.query_text = opts.queryText;
|
|
488
|
+
if (opts.where && Object.keys(opts.where).length > 0) {
|
|
489
|
+
body.where = opts.where;
|
|
490
|
+
}
|
|
491
|
+
if (opts.minSimilarity !== undefined) {
|
|
492
|
+
body.min_similarity = opts.minSimilarity;
|
|
493
|
+
}
|
|
494
|
+
const raw = await this.request('POST', '/v1/sdk/db/search-semantic', body);
|
|
495
|
+
return {
|
|
496
|
+
hits: (raw.results ?? []).map(r => ({
|
|
497
|
+
record: (0, record_1.recordFromWire)(r.record),
|
|
498
|
+
distance: r.distance,
|
|
499
|
+
})),
|
|
500
|
+
total: raw.total ?? (raw.results ?? []).length,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
// ─── Manual sync ────────────────────────────────────────────────────────────
|
|
504
|
+
async syncPendingWrites() {
|
|
505
|
+
await this.syncEngine.flush();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
exports.KoolbaseDatabase = KoolbaseDatabase;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Koolbase React Native SDK version. Sent in the `x-koolbase-sdk-version`
|
|
3
|
+
* header on every authenticated request so the server can route
|
|
4
|
+
* version-conditional logic (deprecation warnings, schema migrations,
|
|
5
|
+
* feature flags). Must match the `version` field in package.json.
|
|
6
|
+
*/
|
|
7
|
+
export declare const koolbaseSdkVersion = "1.11.0";
|
|
8
|
+
/**
|
|
9
|
+
* Builds device-identifying headers attached to every Koolbase auth
|
|
10
|
+
* request. Mirrors the Flutter SDK's `DeviceMetadata` for parity. Apps
|
|
11
|
+
* with privacy concerns can swap in a custom storage adapter to avoid
|
|
12
|
+
* persisting the device label.
|
|
13
|
+
*
|
|
14
|
+
* Headers emitted:
|
|
15
|
+
* - User-Agent: koolbase-react-native/<sdk> (<platform> <version>)
|
|
16
|
+
* - x-koolbase-sdk: react-native
|
|
17
|
+
* - x-koolbase-sdk-version: <koolbaseSdkVersion>
|
|
18
|
+
* - x-koolbase-platform: ios | android | web | etc.
|
|
19
|
+
* - x-koolbase-platform-version: numeric SDK level or OS version string
|
|
20
|
+
* - x-koolbase-app-version: from KoolbaseConfig.appVersion or 'unknown'
|
|
21
|
+
* - x-koolbase-device-label: persistent UUID per install
|
|
22
|
+
*/
|
|
23
|
+
export declare class DeviceMetadata {
|
|
24
|
+
private cached;
|
|
25
|
+
private ephemeralLabel;
|
|
26
|
+
private readonly appVersion;
|
|
27
|
+
constructor(appVersion?: string);
|
|
28
|
+
/**
|
|
29
|
+
* Build (or return cached) device headers. The first call may perform
|
|
30
|
+
* an async keychain read to look up the persisted device label;
|
|
31
|
+
* subsequent calls return the in-memory cache synchronously via the
|
|
32
|
+
* returned Promise.
|
|
33
|
+
*/
|
|
34
|
+
build(): Promise<Record<string, string>>;
|
|
35
|
+
private getOrCreateDeviceLabel;
|
|
36
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DeviceMetadata = exports.koolbaseSdkVersion = void 0;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const auth_storage_1 = require("./auth-storage");
|
|
6
|
+
/**
|
|
7
|
+
* Koolbase React Native SDK version. Sent in the `x-koolbase-sdk-version`
|
|
8
|
+
* header on every authenticated request so the server can route
|
|
9
|
+
* version-conditional logic (deprecation warnings, schema migrations,
|
|
10
|
+
* feature flags). Must match the `version` field in package.json.
|
|
11
|
+
*/
|
|
12
|
+
exports.koolbaseSdkVersion = '1.11.0';
|
|
13
|
+
/**
|
|
14
|
+
* Generate a UUIDv4-shaped string for use as a stable per-install
|
|
15
|
+
* device label. Not cryptographically secure — this is a label, not a
|
|
16
|
+
* security primitive. Avoids pulling in a crypto-grade UUID dependency.
|
|
17
|
+
*/
|
|
18
|
+
function generateDeviceLabel() {
|
|
19
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
20
|
+
const r = (Math.random() * 16) | 0;
|
|
21
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
22
|
+
return v.toString(16);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds device-identifying headers attached to every Koolbase auth
|
|
27
|
+
* request. Mirrors the Flutter SDK's `DeviceMetadata` for parity. Apps
|
|
28
|
+
* with privacy concerns can swap in a custom storage adapter to avoid
|
|
29
|
+
* persisting the device label.
|
|
30
|
+
*
|
|
31
|
+
* Headers emitted:
|
|
32
|
+
* - User-Agent: koolbase-react-native/<sdk> (<platform> <version>)
|
|
33
|
+
* - x-koolbase-sdk: react-native
|
|
34
|
+
* - x-koolbase-sdk-version: <koolbaseSdkVersion>
|
|
35
|
+
* - x-koolbase-platform: ios | android | web | etc.
|
|
36
|
+
* - x-koolbase-platform-version: numeric SDK level or OS version string
|
|
37
|
+
* - x-koolbase-app-version: from KoolbaseConfig.appVersion or 'unknown'
|
|
38
|
+
* - x-koolbase-device-label: persistent UUID per install
|
|
39
|
+
*/
|
|
40
|
+
class DeviceMetadata {
|
|
41
|
+
constructor(appVersion) {
|
|
42
|
+
this.cached = null;
|
|
43
|
+
this.ephemeralLabel = null;
|
|
44
|
+
this.appVersion = appVersion ?? 'unknown';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build (or return cached) device headers. The first call may perform
|
|
48
|
+
* an async keychain read to look up the persisted device label;
|
|
49
|
+
* subsequent calls return the in-memory cache synchronously via the
|
|
50
|
+
* returned Promise.
|
|
51
|
+
*/
|
|
52
|
+
async build() {
|
|
53
|
+
if (this.cached)
|
|
54
|
+
return this.cached;
|
|
55
|
+
const platform = String(react_native_1.Platform.OS);
|
|
56
|
+
const platformVersion = String(react_native_1.Platform.Version);
|
|
57
|
+
const deviceLabel = await this.getOrCreateDeviceLabel();
|
|
58
|
+
const userAgent = `koolbase-react-native/${exports.koolbaseSdkVersion} (${platform} ${platformVersion})`;
|
|
59
|
+
this.cached = {
|
|
60
|
+
'User-Agent': userAgent,
|
|
61
|
+
'x-koolbase-sdk': 'react-native',
|
|
62
|
+
'x-koolbase-sdk-version': exports.koolbaseSdkVersion,
|
|
63
|
+
'x-koolbase-platform': platform,
|
|
64
|
+
'x-koolbase-platform-version': platformVersion,
|
|
65
|
+
'x-koolbase-app-version': this.appVersion,
|
|
66
|
+
'x-koolbase-device-label': deviceLabel,
|
|
67
|
+
};
|
|
68
|
+
return this.cached;
|
|
69
|
+
}
|
|
70
|
+
async getOrCreateDeviceLabel() {
|
|
71
|
+
// No keychain available → ephemeral per-session label.
|
|
72
|
+
// (Better than no label — still useful for in-session debugging.)
|
|
73
|
+
if (!(0, auth_storage_1.isKeychainAvailable)()) {
|
|
74
|
+
if (!this.ephemeralLabel) {
|
|
75
|
+
this.ephemeralLabel = generateDeviceLabel();
|
|
76
|
+
}
|
|
77
|
+
return this.ephemeralLabel;
|
|
78
|
+
}
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
80
|
+
const Keychain = require('react-native-keychain');
|
|
81
|
+
const service = 'koolbase_device_label_v1';
|
|
82
|
+
try {
|
|
83
|
+
const existing = await Keychain.getGenericPassword({ service });
|
|
84
|
+
if (existing && existing.password) {
|
|
85
|
+
return existing.password;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// fall through to create
|
|
90
|
+
}
|
|
91
|
+
const newLabel = generateDeviceLabel();
|
|
92
|
+
try {
|
|
93
|
+
await Keychain.setGenericPassword('device', newLabel, { service });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Persistence failed — return the generated label anyway, but
|
|
97
|
+
// don't cache it as ephemeral since future requests may persist.
|
|
98
|
+
}
|
|
99
|
+
return newLabel;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.DeviceMetadata = DeviceMetadata;
|
package/dist/flags.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { KoolbaseConfig, VersionCheckResult } from './types';
|
|
2
|
+
export declare class KoolbaseFlags {
|
|
3
|
+
private config;
|
|
4
|
+
private payload;
|
|
5
|
+
private deviceId;
|
|
6
|
+
constructor(config: KoolbaseConfig, deviceId: string);
|
|
7
|
+
fetch(appVersion: string, platform: string): Promise<void>;
|
|
8
|
+
isEnabled(key: string): boolean;
|
|
9
|
+
getString(key: string, fallback?: string): string;
|
|
10
|
+
getNumber(key: string, fallback?: number): number;
|
|
11
|
+
getBool(key: string, fallback?: boolean): boolean;
|
|
12
|
+
checkVersion(currentVersion: string): VersionCheckResult;
|
|
13
|
+
private parseVersion;
|
|
14
|
+
private stableHash;
|
|
15
|
+
}
|