@techfinityedge/koolbase-react-native 5.4.0 → 6.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 CHANGED
@@ -379,6 +379,66 @@ When the device is offline, these writes are queued and synced automatically whe
379
379
 
380
380
  ---
381
381
 
382
+ ### Semantic search
383
+
384
+ Find records by meaning, not just by field equality. Store an embedding
385
+ vector on a record, then search the collection for nearest neighbors to a
386
+ query vector — useful for similarity search over user-supplied embeddings.
387
+
388
+ Declare a vector field on the collection from the dashboard or CLI first
389
+ (picking a dimension; v1 supports 384, 768, 1024, and 1536). Then write
390
+ and search from the SDK:
391
+
392
+ ```typescript
393
+ // Set a vector for one record
394
+ await Koolbase.db.setVector(
395
+ articleId,
396
+ 'embedding',
397
+ await myEmbeddingModel.encode(article.content),
398
+ );
399
+
400
+ // Read it back
401
+ const v = await Koolbase.db.getVector(articleId, 'embedding');
402
+ console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
403
+
404
+ // Search the collection for nearest neighbors
405
+ const result = await Koolbase.db.searchSemantic({
406
+ collection: 'articles',
407
+ field: 'embedding',
408
+ queryVector: await myEmbeddingModel.encode(userQuery),
409
+ limit: 10,
410
+ });
411
+
412
+ for (const hit of result.hits) {
413
+ // hit.distance is cosine distance: lower = more similar
414
+ // (0 = identical direction, 1 ≈ orthogonal, 2 = opposite)
415
+ console.log(hit.record.data.title, hit.distance.toFixed(3));
416
+ }
417
+
418
+ // Optionally scope the search with an equality filter
419
+ const scoped = await Koolbase.db.searchSemantic({
420
+ collection: 'articles',
421
+ field: 'embedding',
422
+ queryVector: queryEmbedding,
423
+ limit: 10,
424
+ where: { category: 'tech' },
425
+ });
426
+
427
+ // Remove a record's vector when you no longer need it
428
+ await Koolbase.db.deleteVector(articleId, 'embedding');
429
+ ```
430
+
431
+ A few behaviors worth knowing:
432
+
433
+ - **Vector length must match the declared dimension.** Mismatches throw `KoolbaseVectorDimensionMismatchError` with the expected and actual dimensions in the message.
434
+ - **Online-only.** Vector operations are not cached locally or queued offline — HNSW similarity search has no useful offline semantics, so deferred writes could corrupt your view of what's stored.
435
+ - **Read rule applies post-search.** Semantic search respects the collection's read rule the same way `query()` does: `owner`/`scoped`/`conditional` records are filtered to the caller after the HNSW lookup, so strict rules may return fewer than `limit` results.
436
+ - **Higher dimensions coming.** OpenAI's `text-embedding-3-large` ships at 3072 dimensions, supported in a future release once pgvector is upgraded. In the meantime, use your model's `dimensions=1536` parameter (Matryoshka truncation) for full compatibility.
437
+
438
+ See [Semantic search docs](https://docs.koolbase.com/database/vectors) for setup, dimension guidance, and embedding model recommendations.
439
+
440
+ ---
441
+
382
442
  ## Storage
383
443
 
384
444
  Upload and serve files via presigned URLs to Cloudflare R2. Uploads are
@@ -485,6 +545,64 @@ so nothing leaks.
485
545
 
486
546
  ---
487
547
 
548
+ ### Object versioning
549
+
550
+ For buckets with versioning enabled, every overwrite preserves the prior
551
+ content as a history version, and deletes are soft (recoverable until
552
+ force-purged). Enable versioning on a bucket from the dashboard.
553
+
554
+ ```typescript
555
+ // List all versions of a path, newest first
556
+ const versions = await Koolbase.storage.listVersions('documents', 'contract.pdf');
557
+
558
+ for (const v of versions) {
559
+ console.log(`${v.versionId}: size=${v.size} isCurrent=${v.isCurrent}`);
560
+ }
561
+
562
+ // Download a specific historical version
563
+ const url = await Koolbase.storage.getDownloadUrl(
564
+ 'documents',
565
+ 'contract.pdf',
566
+ '019e98ed-eed6-7e71-...',
567
+ );
568
+
569
+ // Bring a history version back as current
570
+ // (the existing current is snapshotted to history first)
571
+ const restored = await Koolbase.storage.restoreVersion(
572
+ 'documents',
573
+ 'contract.pdf',
574
+ '019e98ed-eed6-7e71-...',
575
+ );
576
+
577
+ // Hard-remove a single history version (row + R2 bytes)
578
+ await Koolbase.storage.purgeVersion(
579
+ 'documents',
580
+ 'contract.pdf',
581
+ 'old-version-id',
582
+ );
583
+
584
+ // Wipe the entire timeline for a path - every version, every R2 key
585
+ await Koolbase.storage.delete('documents', 'contract.pdf', true);
586
+ ```
587
+
588
+ A few behaviors worth knowing:
589
+
590
+ - **Overwrite snapshots automatically.** Upload to a path that already
591
+ exists in a versioned bucket and the prior bytes are preserved as
592
+ history; the upload becomes the new current.
593
+ - **Delete is soft by default.** On a versioned bucket, `delete`
594
+ snapshots the current content and records a delete marker. The
595
+ content is still recoverable via `restoreVersion` until force-purged.
596
+ - **Restore is itself a versioned event.** The previously-current row
597
+ gets snapshotted before the target's bytes overwrite canonical. The
598
+ restored row gets a fresh `versionId`; the target stays in history at
599
+ its original id - so you can always undo a restore.
600
+ - **Delete markers can be listed but not downloaded.** A marker has
601
+ `size === 0`, `isDeleteMarker === true`, and no bytes. Calling
602
+ `getDownloadUrl` with a marker's `versionId` throws.
603
+
604
+ ---
605
+
488
606
  ## Realtime
489
607
 
490
608
  Subscribe to live changes on a collection. Uses the signed-in user's session, so
@@ -697,13 +815,13 @@ handling doesn't depend on message text.
697
815
  All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
698
816
 
699
817
  | Error | When |
700
- | `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` — the colliding path. |
701
- | `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
702
- | `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
703
- | `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
704
- | `KoolbaseStorageQuotaError` | An upload would push the bucket past its `max_size_bytes` cap (409, code `QUOTA_EXCEEDED`). |
705
- | `KoolbaseStorageFileTooLargeError` | A single file exceeds the bucket's `max_file_size_bytes` cap (413, code `FILE_TOO_LARGE`). |
706
- | `KoolbaseStorageMimeTypeError` | The upload's content-type isn't in the bucket's `allowed_mime_types` allowlist (415, code `MIME_NOT_ALLOWED`). |
818
+ |---|---|
819
+ | `KoolbaseConflictError` | A write violates a unique constraint (409). Exposes `.field` — the field that collided, when the server reports it. |
820
+ | `KoolbaseNotFoundError` | The record or collection doesn't exist (404). |
821
+ | `KoolbaseValidationError` | The request was rejected as invalid (400). |
822
+ | `KoolbasePermissionError` | An access rule denied the operation (403). |
823
+ | `KoolbaseRateLimitError` | The caller is being rate-limited (429). |
824
+ | `KoolbaseVectorDimensionMismatchError` | A vector's length doesn't match the field's declared dimension (400, code `vector_dimension_mismatch`). |
707
825
 
708
826
  ```ts
709
827
  import { KoolbaseConflictError, KoolbaseDataError } from '@techfinityedge/koolbase-react-native';
@@ -770,8 +888,8 @@ try {
770
888
  ## What's included
771
889
 
772
890
  - Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
773
- - Database with offline-first cache, realtime subscriptions, and populate
774
- - Storage with presigned uploads and downloads, safe-by-default conflict handling
891
+ - Database with offline-first cache, realtime subscriptions, populate for related records, semantic search over vectors
892
+ - Storage with presigned uploads and downloads, safe-by-default conflict handling, image transforms, object versioning (history + restore + soft-delete)
775
893
  - Realtime subscriptions over WebSocket
776
894
  - Authenticated functions (`ctx.auth` exposes the caller automatically)
777
895
  - Feature flags and remote config
@@ -69,6 +69,24 @@ export declare class KoolbasePermissionError extends KoolbaseDataError {
69
69
  export declare class KoolbaseRateLimitError extends KoolbaseDataError {
70
70
  constructor(message?: string);
71
71
  }
72
+ /**
73
+ * Thrown when the supplied vector's length does not match the dimension
74
+ * declared on the collection's vector field — the server responds with
75
+ * 400 and code `vector_dimension_mismatch`. The message includes both
76
+ * the expected and actual dimensions so you can surface a precise error.
77
+ *
78
+ * @example
79
+ * try {
80
+ * await koolbase.db.setVector(id, 'embedding', [0.1, 0.2]); // 2 dims
81
+ * } catch (e) {
82
+ * if (e instanceof KoolbaseVectorDimensionMismatchError) {
83
+ * showError(e.message); // "expected 1536, got 2"
84
+ * }
85
+ * }
86
+ */
87
+ export declare class KoolbaseVectorDimensionMismatchError extends KoolbaseDataError {
88
+ constructor(message?: string);
89
+ }
72
90
  /**
73
91
  * Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
74
92
  * preferring the server's stable `code` and falling back to the HTTP status
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.KoolbaseRateLimitError = exports.KoolbasePermissionError = exports.KoolbaseValidationError = exports.KoolbaseNotFoundError = exports.KoolbaseConflictError = exports.KoolbaseDataError = void 0;
3
+ exports.KoolbaseVectorDimensionMismatchError = exports.KoolbaseRateLimitError = exports.KoolbasePermissionError = exports.KoolbaseValidationError = exports.KoolbaseNotFoundError = exports.KoolbaseConflictError = exports.KoolbaseDataError = void 0;
4
4
  exports.koolbaseDataError = koolbaseDataError;
5
5
  /**
6
6
  * Base class for errors surfaced by the Koolbase data layer (database reads
@@ -103,6 +103,29 @@ class KoolbaseRateLimitError extends KoolbaseDataError {
103
103
  }
104
104
  }
105
105
  exports.KoolbaseRateLimitError = KoolbaseRateLimitError;
106
+ /**
107
+ * Thrown when the supplied vector's length does not match the dimension
108
+ * declared on the collection's vector field — the server responds with
109
+ * 400 and code `vector_dimension_mismatch`. The message includes both
110
+ * the expected and actual dimensions so you can surface a precise error.
111
+ *
112
+ * @example
113
+ * try {
114
+ * await koolbase.db.setVector(id, 'embedding', [0.1, 0.2]); // 2 dims
115
+ * } catch (e) {
116
+ * if (e instanceof KoolbaseVectorDimensionMismatchError) {
117
+ * showError(e.message); // "expected 1536, got 2"
118
+ * }
119
+ * }
120
+ */
121
+ class KoolbaseVectorDimensionMismatchError extends KoolbaseDataError {
122
+ constructor(message) {
123
+ super(message ?? 'Vector dimension does not match field declaration', 'vector_dimension_mismatch');
124
+ this.name = 'KoolbaseVectorDimensionMismatchError';
125
+ Object.setPrototypeOf(this, KoolbaseVectorDimensionMismatchError.prototype);
126
+ }
127
+ }
128
+ exports.KoolbaseVectorDimensionMismatchError = KoolbaseVectorDimensionMismatchError;
106
129
  /**
107
130
  * Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
108
131
  * preferring the server's stable `code` and falling back to the HTTP status
@@ -119,13 +142,19 @@ function koolbaseDataError(status, body, fallbackMessage = 'Request failed') {
119
142
  case 'not_found':
120
143
  case 'record_not_found':
121
144
  case 'collection_not_found':
145
+ case 'vector_not_found':
146
+ case 'vector_field_not_found':
122
147
  return new KoolbaseNotFoundError(message);
123
148
  case 'permission_denied':
124
149
  return new KoolbasePermissionError(message);
125
150
  case 'rate_limit':
126
151
  return new KoolbaseRateLimitError(message);
127
152
  case 'validation_error':
153
+ case 'vector_collection_mismatch':
154
+ case 'unsupported_dimension':
128
155
  return new KoolbaseValidationError(message);
156
+ case 'vector_dimension_mismatch':
157
+ return new KoolbaseVectorDimensionMismatchError(message);
129
158
  }
130
159
  // ─── status fallback (pre-code servers) ───
131
160
  switch (status) {
@@ -1,4 +1,4 @@
1
- import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult, UpsertResult, BatchOp, BatchResult } from './types';
1
+ import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult, UpsertResult, BatchOp, BatchResult, KoolbaseVector, SemanticSearchResult } from './types';
2
2
  export declare class KoolbaseDatabase {
3
3
  private config;
4
4
  private getUserId;
@@ -90,5 +90,83 @@ export declare class KoolbaseDatabase {
90
90
  */
91
91
  update(recordId: string, data: Record<string, unknown>): Promise<KoolbaseRecord>;
92
92
  delete(recordId: string): Promise<void>;
93
+ /**
94
+ * Write (or replace) a vector for a record on the named `field`.
95
+ *
96
+ * The field must already be declared on the collection via the dashboard
97
+ * or CLI. `vector.length` must match the field's declared dimension;
98
+ * otherwise throws `KoolbaseVectorDimensionMismatchError`.
99
+ *
100
+ * Online-only — vectors are not cached locally or queued offline because
101
+ * HNSW similarity search has no useful offline semantics.
102
+ *
103
+ * @example
104
+ * await Koolbase.db.setVector(
105
+ * articleId,
106
+ * 'embedding',
107
+ * await myEmbeddingModel.encode(article.content),
108
+ * );
109
+ */
110
+ setVector(recordId: string, field: string, vector: number[]): Promise<void>;
111
+ /**
112
+ * Read a record's stored vector on the named `field`.
113
+ *
114
+ * Throws `KoolbaseNotFoundError` if either the field is not declared or
115
+ * no vector has been set for this record on this field. Throws
116
+ * `KoolbasePermissionError` if the caller cannot read this record per
117
+ * the collection's read rule.
118
+ *
119
+ * Online-only.
120
+ *
121
+ * @example
122
+ * const v = await Koolbase.db.getVector(articleId, 'embedding');
123
+ * console.log(`${v.vector.length}-dim, updated ${v.updatedAt}`);
124
+ */
125
+ getVector(recordId: string, field: string): Promise<KoolbaseVector>;
126
+ /**
127
+ * Remove a record's stored vector on the named `field`.
128
+ *
129
+ * Online-only. Throws `KoolbaseNotFoundError` if no vector is set for
130
+ * `(recordId, field)`; throws `KoolbasePermissionError` if the caller
131
+ * cannot write this record per the collection's write rule.
132
+ *
133
+ * Note: this removes the vector from the dimension table but does NOT
134
+ * remove the field declaration itself — the field stays on the
135
+ * collection and is still settable on other records.
136
+ */
137
+ deleteVector(recordId: string, field: string): Promise<void>;
138
+ /**
139
+ * Semantic search via HNSW vector similarity.
140
+ *
141
+ * Ranks records in `collection` by cosine distance between the supplied
142
+ * `queryVector` and each record's stored vector on `field`. Returns up
143
+ * to `limit` (default 20) nearest hits, with the collection's read rule
144
+ * applied — owner/scoped/conditional records are filtered to the caller.
145
+ *
146
+ * `where` is an optional equality filter map on record `data` fields,
147
+ * applied AFTER the HNSW lookup, so very strict filters may return
148
+ * fewer than `limit` results.
149
+ *
150
+ * Online-only.
151
+ *
152
+ * @example
153
+ * const result = await Koolbase.db.searchSemantic({
154
+ * collection: 'articles',
155
+ * field: 'embedding',
156
+ * queryVector: await myEmbeddingModel.encode(userQuery),
157
+ * limit: 10,
158
+ * where: { category: 'tech' },
159
+ * });
160
+ * for (const hit of result.hits) {
161
+ * console.log(hit.record.data.title, hit.distance);
162
+ * }
163
+ */
164
+ searchSemantic(opts: {
165
+ collection: string;
166
+ field: string;
167
+ queryVector: number[];
168
+ limit?: number;
169
+ where?: Record<string, unknown>;
170
+ }): Promise<SemanticSearchResult>;
93
171
  syncPendingWrites(): Promise<void>;
94
172
  }
package/dist/database.js CHANGED
@@ -319,6 +319,126 @@ class KoolbaseDatabase {
319
319
  // Queued for sync — will retry when online
320
320
  }
321
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
+ * Semantic search via HNSW vector similarity.
399
+ *
400
+ * Ranks records in `collection` by cosine distance between the supplied
401
+ * `queryVector` and each record's stored vector on `field`. Returns up
402
+ * to `limit` (default 20) nearest hits, with the collection's read rule
403
+ * applied — owner/scoped/conditional records are filtered to the caller.
404
+ *
405
+ * `where` is an optional equality filter map on record `data` fields,
406
+ * applied AFTER the HNSW lookup, so very strict filters may return
407
+ * fewer than `limit` results.
408
+ *
409
+ * Online-only.
410
+ *
411
+ * @example
412
+ * const result = await Koolbase.db.searchSemantic({
413
+ * collection: 'articles',
414
+ * field: 'embedding',
415
+ * queryVector: await myEmbeddingModel.encode(userQuery),
416
+ * limit: 10,
417
+ * where: { category: 'tech' },
418
+ * });
419
+ * for (const hit of result.hits) {
420
+ * console.log(hit.record.data.title, hit.distance);
421
+ * }
422
+ */
423
+ async searchSemantic(opts) {
424
+ const body = {
425
+ collection: opts.collection,
426
+ field: opts.field,
427
+ query_vector: opts.queryVector,
428
+ limit: opts.limit ?? 20,
429
+ };
430
+ if (opts.where && Object.keys(opts.where).length > 0) {
431
+ body.where = opts.where;
432
+ }
433
+ const raw = await this.request('POST', '/v1/sdk/db/search-semantic', body);
434
+ return {
435
+ hits: (raw.results ?? []).map(r => ({
436
+ record: (0, record_1.recordFromWire)(r.record),
437
+ distance: r.distance,
438
+ })),
439
+ total: raw.total ?? (raw.results ?? []).length,
440
+ };
441
+ }
322
442
  // ─── Manual sync ────────────────────────────────────────────────────────────
323
443
  async syncPendingWrites() {
324
444
  await this.syncEngine.flush();
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { KoolbaseConfig, UploadOptions, UploadResult, KoolbaseObject, KoolbaseImageTransform } from './types';
1
+ import { KoolbaseConfig, UploadOptions, UploadResult, KoolbaseObject, KoolbaseObjectVersion, KoolbaseImageTransform } from './types';
2
2
  /**
3
3
  * Koolbase storage client — uploads, downloads, and deletes via presigned
4
4
  * Cloudflare R2 URLs.
@@ -72,7 +72,7 @@ export declare class KoolbaseStorage {
72
72
  /**
73
73
  * Get a signed download URL for a file.
74
74
  */
75
- getDownloadUrl(bucket: string, path: string): Promise<string>;
75
+ getDownloadUrl(bucket: string, path: string, versionId?: string): Promise<string>;
76
76
  /**
77
77
  * Build the stable public CDN URL for a file in a public bucket.
78
78
  *
@@ -145,5 +145,40 @@ export declare class KoolbaseStorage {
145
145
  /**
146
146
  * Delete a file from a bucket.
147
147
  */
148
- delete(bucket: string, path: string): Promise<void>;
148
+ delete(bucket: string, path: string, forcePurge?: boolean): Promise<void>;
149
+ /**
150
+ * List all versions of a file path, newest-first. Returns a flat list
151
+ * mixing the current row (with `isCurrent: true`) and all history
152
+ * rows. Delete markers are included so callers can render the full
153
+ * timeline; filter client-side to hide them if the UI only wants
154
+ * restorable versions.
155
+ *
156
+ * Returns an empty array (not an error) when the path has no history
157
+ * and no current row.
158
+ */
159
+ listVersions(bucket: string, path: string): Promise<KoolbaseObjectVersion[]>;
160
+ /**
161
+ * Fetch metadata for a single version by id. Works against both the
162
+ * current row and any history row — the response's `isCurrent` tells
163
+ * you which.
164
+ */
165
+ getVersion(bucket: string, path: string, versionId: string): Promise<KoolbaseObjectVersion>;
166
+ /**
167
+ * Bring a history version back as the current version. The
168
+ * previously-current row (if any) is snapshotted into history first,
169
+ * so this operation is itself a versioned event you can undo. The
170
+ * restored row gets a freshly-minted version_id; the target stays in
171
+ * history at its original version_id.
172
+ *
173
+ * Throws if the bucket has versioning off, if the target is the
174
+ * already-current version, or if the target is a delete marker.
175
+ */
176
+ restoreVersion(bucket: string, path: string, versionId: string): Promise<KoolbaseObject>;
177
+ /**
178
+ * Hard-remove a single history version — both the metadata row and
179
+ * the .versions/ R2 bytes (or just the row, for delete markers).
180
+ * Refuses to operate on the current version; use {@link delete} with
181
+ * `forcePurge: true` to wipe everything for a path.
182
+ */
183
+ purgeVersion(bucket: string, path: string, versionId: string): Promise<void>;
149
184
  }
package/dist/storage.js CHANGED
@@ -195,9 +195,12 @@ class KoolbaseStorage {
195
195
  /**
196
196
  * Get a signed download URL for a file.
197
197
  */
198
- async getDownloadUrl(bucket, path) {
199
- const url = `${this.config.baseUrl}/v1/sdk/storage/download-url` +
198
+ async getDownloadUrl(bucket, path, versionId) {
199
+ let url = `${this.config.baseUrl}/v1/sdk/storage/download-url` +
200
200
  `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
201
+ if (versionId) {
202
+ url += `&version_id=${encodeURIComponent(versionId)}`;
203
+ }
201
204
  const res = await fetch(url, { headers: await this.buildHeaders() });
202
205
  if (!res.ok) {
203
206
  throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to get download URL');
@@ -289,8 +292,11 @@ class KoolbaseStorage {
289
292
  /**
290
293
  * Delete a file from a bucket.
291
294
  */
292
- async delete(bucket, path) {
293
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/object`, {
295
+ async delete(bucket, path, forcePurge) {
296
+ const url = forcePurge
297
+ ? `${this.config.baseUrl}/v1/sdk/storage/object?force_purge=true`
298
+ : `${this.config.baseUrl}/v1/sdk/storage/object`;
299
+ const res = await fetch(url, {
294
300
  method: 'DELETE',
295
301
  headers: {
296
302
  ...(await this.buildHeaders()),
@@ -304,6 +310,82 @@ class KoolbaseStorage {
304
310
  throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to delete file');
305
311
  }
306
312
  }
313
+ /**
314
+ * List all versions of a file path, newest-first. Returns a flat list
315
+ * mixing the current row (with `isCurrent: true`) and all history
316
+ * rows. Delete markers are included so callers can render the full
317
+ * timeline; filter client-side to hide them if the UI only wants
318
+ * restorable versions.
319
+ *
320
+ * Returns an empty array (not an error) when the path has no history
321
+ * and no current row.
322
+ */
323
+ async listVersions(bucket, path) {
324
+ const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions` +
325
+ `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
326
+ const res = await fetch(url, { headers: await this.buildHeaders() });
327
+ if (!res.ok) {
328
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to list versions');
329
+ }
330
+ const data = (await res.json());
331
+ const list = Array.isArray(data.versions) ? data.versions : [];
332
+ return list.map((v) => fromVersionJson(v));
333
+ }
334
+ /**
335
+ * Fetch metadata for a single version by id. Works against both the
336
+ * current row and any history row — the response's `isCurrent` tells
337
+ * you which.
338
+ */
339
+ async getVersion(bucket, path, versionId) {
340
+ const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
341
+ `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
342
+ const res = await fetch(url, { headers: await this.buildHeaders() });
343
+ if (!res.ok) {
344
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to fetch version');
345
+ }
346
+ return fromVersionJson((await res.json()));
347
+ }
348
+ /**
349
+ * Bring a history version back as the current version. The
350
+ * previously-current row (if any) is snapshotted into history first,
351
+ * so this operation is itself a versioned event you can undo. The
352
+ * restored row gets a freshly-minted version_id; the target stays in
353
+ * history at its original version_id.
354
+ *
355
+ * Throws if the bucket has versioning off, if the target is the
356
+ * already-current version, or if the target is a delete marker.
357
+ */
358
+ async restoreVersion(bucket, path, versionId) {
359
+ const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}/restore` +
360
+ `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
361
+ const res = await fetch(url, {
362
+ method: 'POST',
363
+ headers: await this.buildHeaders(),
364
+ });
365
+ if (!res.ok) {
366
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to restore version');
367
+ }
368
+ return mapObjectFromServer(await res.json());
369
+ }
370
+ /**
371
+ * Hard-remove a single history version — both the metadata row and
372
+ * the .versions/ R2 bytes (or just the row, for delete markers).
373
+ * Refuses to operate on the current version; use {@link delete} with
374
+ * `forcePurge: true` to wipe everything for a path.
375
+ */
376
+ async purgeVersion(bucket, path, versionId) {
377
+ const url = `${this.config.baseUrl}/v1/sdk/storage/object-versions/${encodeURIComponent(versionId)}` +
378
+ `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
379
+ const res = await fetch(url, {
380
+ method: 'DELETE',
381
+ headers: await this.buildHeaders(),
382
+ });
383
+ if (res.status === 204)
384
+ return;
385
+ if (!res.ok) {
386
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to purge version');
387
+ }
388
+ }
307
389
  }
308
390
  exports.KoolbaseStorage = KoolbaseStorage;
309
391
  /**
@@ -327,3 +409,30 @@ function mapObjectFromServer(raw) {
327
409
  updatedAt: raw.updated_at,
328
410
  };
329
411
  }
412
+ /**
413
+ * Maps the snake_case server JSON of a version row to the camelCase
414
+ * {@link KoolbaseObjectVersion}. Mirrors `fromObjectJson` shape.
415
+ */
416
+ function fromVersionJson(j) {
417
+ const rawMeta = j.metadata;
418
+ const metadata = {};
419
+ if (rawMeta && typeof rawMeta === 'object') {
420
+ for (const [k, v] of Object.entries(rawMeta)) {
421
+ if (typeof v === 'string')
422
+ metadata[k] = v;
423
+ }
424
+ }
425
+ return {
426
+ versionId: j.version_id ?? null,
427
+ path: j.path,
428
+ size: Number(j.size ?? 0),
429
+ contentType: j.content_type ?? null,
430
+ etag: j.etag ?? null,
431
+ metadata,
432
+ r2Bucket: j.r2_bucket ?? '',
433
+ userId: j.user_id ?? null,
434
+ isDeleteMarker: Boolean(j.is_delete_marker),
435
+ isCurrent: Boolean(j.is_current),
436
+ createdAt: j.created_at,
437
+ };
438
+ }
package/dist/types.d.ts CHANGED
@@ -147,6 +147,41 @@ export interface PendingWrite {
147
147
  retries: number;
148
148
  createdAt: string;
149
149
  }
150
+ /**
151
+ * A stored vector retrieved by `KoolbaseDatabase.getVector()`. The `vector`
152
+ * field carries the float values exactly as stored on the server; the
153
+ * `recordId` + `fieldName` pair identifies which slot they came from.
154
+ */
155
+ export interface KoolbaseVector {
156
+ recordId: string;
157
+ fieldName: string;
158
+ vector: number[];
159
+ /** ISO 8601 timestamp from the server. */
160
+ createdAt: string;
161
+ /** ISO 8601 timestamp from the server. */
162
+ updatedAt: string;
163
+ }
164
+ /**
165
+ * One ranked hit from `KoolbaseDatabase.searchSemantic()`. `record` is
166
+ * the full record (same wire shape as a record returned by query/get).
167
+ * `distance` is the cosine distance between the query vector and the
168
+ * stored vector — lower means more similar. Range: 0 (identical
169
+ * direction) to 2 (opposite direction).
170
+ */
171
+ export interface KoolbaseSemanticHit {
172
+ record: KoolbaseRecord;
173
+ distance: number;
174
+ }
175
+ /**
176
+ * Result of `KoolbaseDatabase.searchSemantic()`. `hits` is the ranked
177
+ * list of nearest neighbors (best match first); `total` is the count of
178
+ * hits returned (matches `hits.length` in v1 — preserved as a separate
179
+ * field for future pagination).
180
+ */
181
+ export interface SemanticSearchResult {
182
+ hits: KoolbaseSemanticHit[];
183
+ total: number;
184
+ }
150
185
  export interface UploadOptions {
151
186
  bucket: string;
152
187
  path: string;
@@ -212,6 +247,48 @@ export interface KoolbaseObject {
212
247
  /** ISO 8601 timestamp from the server. */
213
248
  updatedAt: string;
214
249
  }
250
+ /**
251
+ * One entry in an object's version timeline. Covers both the current
252
+ * row (when {@link isCurrent} is true) and every history row, including
253
+ * soft-delete markers (when {@link isDeleteMarker} is true — size 0, no
254
+ * fetchable bytes). Returned from {@link KoolbaseStorage.listVersions}
255
+ * and {@link KoolbaseStorage.getVersion}; the underlying bytes are
256
+ * downloadable via {@link KoolbaseStorage.getDownloadUrl} with the
257
+ * `versionId` argument.
258
+ *
259
+ * `versionId` may be null only on legacy rows uploaded before versioning
260
+ * was enabled on the bucket — for those, {@link isCurrent} is true and
261
+ * the row carries no history identity yet (gets backfilled on the next
262
+ * overwrite).
263
+ */
264
+ export interface KoolbaseObjectVersion {
265
+ versionId: string | null;
266
+ path: string;
267
+ size: number;
268
+ contentType: string | null;
269
+ etag: string | null;
270
+ metadata: Record<string, string>;
271
+ r2Bucket: string;
272
+ userId: string | null;
273
+ /**
274
+ * True for a tombstone row recording a soft-delete event. Size is 0
275
+ * and there are no R2 bytes — treat as "the path was deleted at this
276
+ * time" rather than fetchable content.
277
+ */
278
+ isDeleteMarker: boolean;
279
+ /**
280
+ * True for the row that currently lives in `storage_objects` (i.e.
281
+ * what a no-versionId download returns). False for everything in
282
+ * `storage_object_versions`.
283
+ */
284
+ isCurrent: boolean;
285
+ /**
286
+ * For the current row this is the time the current version became
287
+ * current (overwrite or upload time). For history rows it's the time
288
+ * the version was originally uploaded.
289
+ */
290
+ createdAt: string;
291
+ }
215
292
  /**
216
293
  * Result of a successful `KoolbaseStorage.upload()` call.
217
294
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "5.4.0",
3
+ "version": "6.0.0",
4
4
  "description": "React Native SDK for Koolbase — auth, database, storage, realtime, feature flags, and functions in one package.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",