@techfinityedge/koolbase-react-native 4.2.1 → 5.1.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
@@ -16,24 +16,24 @@ Auth, database, storage, realtime, functions, feature flags, remote config, vers
16
16
  3. Add the SDK:
17
17
 
18
18
  ```bash
19
- npm install @techfinityedge/koolbase-react-native
20
- # or
21
- yarn add @techfinityedge/koolbase-react-native
22
- # or
23
- pnpm add @techfinityedge/koolbase-react-native
24
- # or
25
- bun add @techfinityedge/koolbase-react-native
19
+ npm install @techfinityedge/koolbase-react-native
20
+ # or
21
+ yarn add @techfinityedge/koolbase-react-native
22
+ # or
23
+ pnpm add @techfinityedge/koolbase-react-native
24
+ # or
25
+ bun add @techfinityedge/koolbase-react-native
26
26
  ```
27
27
 
28
- **4. Initialize at app startup:**
28
+ 4. Initialize at app startup:
29
29
 
30
30
  ```typescript
31
- import { Koolbase } from '@techfinityedge/koolbase-react-native';
31
+ import { Koolbase } from '@techfinityedge/koolbase-react-native';
32
32
 
33
- await Koolbase.initialize({
34
- publicKey: 'pk_live_xxxx',
35
- baseUrl: 'https://api.koolbase.com',
36
- });
33
+ await Koolbase.initialize({
34
+ publicKey: 'pk_live_xxxx',
35
+ baseUrl: 'https://api.koolbase.com',
36
+ });
37
37
  ```
38
38
 
39
39
  That's it. Every feature below is now available via `Koolbase.*`.
@@ -125,17 +125,17 @@ Configure Google Sign-In for your environment with the OAuth client IDs from Goo
125
125
 
126
126
  ```typescript
127
127
  // Send a one-time code
128
- await Koolbase.auth.sendOtp({ phoneE164: '+233200000000' });
128
+ await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
129
129
 
130
130
  // Verify and sign in
131
131
  await Koolbase.auth.verifyOtp({
132
- phoneE164: '+233200000000',
132
+ phoneNumber: '+233200000000',
133
133
  code: '123456',
134
134
  });
135
135
 
136
136
  // Or link a phone to an existing account
137
137
  await Koolbase.auth.linkPhone({
138
- phoneE164: '+233200000000',
138
+ phoneNumber: '+233200000000',
139
139
  code: '123456',
140
140
  });
141
141
  ```
@@ -175,23 +175,23 @@ await Koolbase.db.delete('record-id');
175
175
 
176
176
  ### Handling unique-constraint conflicts
177
177
 
178
- A write that would violate a unique constraint throws `KoolbaseConflictError`:
178
+ A write that would violate a unique constraint throws `KoolbaseConflictError`:
179
179
 
180
- \`\`\`ts
181
- try {
182
- await koolbase.db.upsert('users', { email }, { name });
183
- } catch (e) {
184
- if (e instanceof KoolbaseConflictError) {
185
- showError('That email is already registered.');
186
- }
180
+ ```ts
181
+ try {
182
+ await Koolbase.db.upsert('users', { email }, { name });
183
+ } catch (e) {
184
+ if (e instanceof KoolbaseConflictError) {
185
+ showError('That email is already registered.');
187
186
  }
188
- \`\`\`
187
+ }
188
+ ```
189
189
 
190
190
  ### Upsert
191
191
 
192
192
  Insert a record, or update the existing one matching a filter.
193
193
 
194
- \`\`\`ts
194
+ ```ts
195
195
  const result = await Koolbase.db.upsert(
196
196
  'profiles',
197
197
  { user_id: userId },
@@ -200,7 +200,7 @@ const result = await Koolbase.db.upsert(
200
200
 
201
201
  console.log(result.created); // true if inserted, false if updated
202
202
  console.log(result.record.id);
203
- \`\`\`
203
+ ```
204
204
 
205
205
  > Online-only: needs the server's view to decide insert vs update, so unlike
206
206
  > `insert` it isn't queued offline and throws on network failure.
@@ -209,12 +209,12 @@ console.log(result.record.id);
209
209
 
210
210
  Bulk-delete every record matching a filter. Returns the number deleted.
211
211
 
212
- \`\`\`ts
212
+ ```ts
213
213
  const deleted = await Koolbase.db.deleteWhere('sessions', {
214
214
  user_id: userId,
215
215
  status: 'expired',
216
216
  });
217
- \`\`\`
217
+ ```
218
218
 
219
219
  > A non-empty filter is required. The collection's delete rule applies; for
220
220
  > `owner`/`scoped` rules the delete is scoped to your own records. Online-only.
@@ -253,7 +253,7 @@ const results = await Koolbase.db.batch([
253
253
  // - delete: { type, deleted: true }
254
254
  ```
255
255
 
256
- **Online-only by design.** Atomicity needs the server's authoritative view, so `batch()` is never queued offline — it throws on network failure (like `upsert` and `deleteWhere`). A server-side rejection throws a `KoolbaseDataException` with the failing operation's details; nothing was persisted.
256
+ **Online-only by design.** Atomicity needs the server's authoritative view, so `batch()` is never queued offline — it throws on network failure (like `upsert` and `deleteWhere`). A server-side rejection throws a `KoolbaseDataError` with the failing operation's details; nothing was persisted.
257
257
 
258
258
  ---
259
259
 
@@ -281,18 +281,106 @@ When the device is offline, these writes are queued and synced automatically whe
281
281
 
282
282
  ## Storage
283
283
 
284
+ Upload and serve files via presigned URLs to Cloudflare R2. Uploads are
285
+ **safe-by-default** (v5+) — uploading to a path that's already taken throws
286
+ `KoolbaseStorageConflictError` instead of silently replacing the existing
287
+ file. Pass `overwrite: true` for true upsert semantics.
288
+
284
289
  ```typescript
285
- const { url } = await Koolbase.storage.upload({
290
+ // Upload rejects if `user-${userId}.jpg` already exists
291
+ const { object, downloadUrl } = await Koolbase.storage.upload({
286
292
  bucket: 'avatars',
287
293
  path: `user-${userId}.jpg`,
288
294
  file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
289
295
  });
290
296
 
291
- const downloadUrl = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
297
+ // Upload silently replaces any existing object at this path
298
+ await Koolbase.storage.upload({
299
+ bucket: 'avatars',
300
+ path: `user-${userId}.jpg`,
301
+ file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
302
+ overwrite: true,
303
+ });
292
304
 
305
+ // Get download URL
306
+ const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
307
+
308
+ // Delete
293
309
  await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
294
310
  ```
295
311
 
312
+ ### Handling upload conflicts
313
+
314
+ For user-supplied filenames, prompt the user before overwriting:
315
+
316
+ ```typescript
317
+ import { KoolbaseStorageConflictError } from '@techfinityedge/koolbase-react-native';
318
+
319
+ try {
320
+ await Koolbase.storage.upload({
321
+ bucket: 'documents',
322
+ path: filename,
323
+ file: { uri, name: filename, type: mimeType },
324
+ });
325
+ } catch (e) catch (e) {
326
+ if (e instanceof KoolbaseStorageConflictError) {
327
+ const ok = await confirm(${e.path} already exists. Overwrite?);
328
+ if (ok) {
329
+ await Koolbase.storage.upload({
330
+ bucket: 'documents',
331
+ path: filename,
332
+ file: { uri, name: filename, type: mimeType },
333
+ overwrite: true,
334
+ });
335
+ }
336
+ } else {
337
+ throw e;
338
+ }
339
+ }
340
+ ```
341
+
342
+ See [Error handling](#error-handling) for the full set of storage errors.
343
+
344
+ ---
345
+
346
+ ### Handling bucket limits
347
+
348
+ Buckets can be configured at creation time with a total size cap
349
+ (`max_size_bytes`), a per-file cap (`max_file_size_bytes`), and a
350
+ content-type allowlist (`allowed_mime_types`, supports `image/*`-style
351
+ wildcards). The server surfaces violations as typed errors:
352
+
353
+ ````typescript
354
+ import {
355
+ KoolbaseStorageQuotaError,
356
+ KoolbaseStorageFileTooLargeError,
357
+ KoolbaseStorageMimeTypeError,
358
+ } from '@techfinityedge/koolbase-react-native';
359
+
360
+ try {
361
+ await Koolbase.storage.upload({
362
+ bucket: 'user-photos',
363
+ path: filename,
364
+ file: { uri, name: filename, type: mimeType },
365
+ });
366
+ } catch (e) {
367
+ if (e instanceof KoolbaseStorageMimeTypeError) {
368
+ showError('That file type is not allowed in this bucket.');
369
+ } else if (e instanceof KoolbaseStorageFileTooLargeError) {
370
+ showError('That file is too big — pick a smaller one.');
371
+ } else if (e instanceof KoolbaseStorageQuotaError) {
372
+ showError('This bucket is full — delete some files and try again.');
373
+ } else {
374
+ throw e;
375
+ }
376
+ }
377
+ ````
378
+
379
+ MIME enforcement runs at presign time — no bytes are transferred before
380
+ rejection. File-size and quota enforcement run at confirm time; the
381
+ server cleans up the underlying R2 object before returning the error,
382
+ so nothing leaks.
383
+
296
384
  ---
297
385
 
298
386
  ## Realtime
@@ -315,7 +403,7 @@ unsubscribe();
315
403
  ```
316
404
 
317
405
  The socket opens lazily, is shared, and reconnects automatically. The project is
318
- taken from the user's session..
406
+ taken from the user's session.
319
407
 
320
408
  ---
321
409
 
@@ -507,18 +595,19 @@ handling doesn't depend on message text.
507
595
  All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
508
596
 
509
597
  | Error | When |
510
- |---|---|
511
- | `KoolbaseConflictError` | A write violates a unique constraint (409). Exposes `.field`. |
512
- | `KoolbaseNotFoundError` | The record or collection doesn't exist (404). |
513
- | `KoolbaseValidationError` | The request was rejected as invalid (400). |
514
- | `KoolbasePermissionError` | An access rule denied the operation (403). |
515
- | `KoolbaseRateLimitError` | The caller is being rate-limited (429). |
598
+ | `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` — the colliding path. |
599
+ | `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
600
+ | `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
601
+ | `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
602
+ | `KoolbaseStorageQuotaError` | An upload would push the bucket past its `max_size_bytes` cap (409, code `QUOTA_EXCEEDED`). |
603
+ | `KoolbaseStorageFileTooLargeError` | A single file exceeds the bucket's `max_file_size_bytes` cap (413, code `FILE_TOO_LARGE`). |
604
+ | `KoolbaseStorageMimeTypeError` | The upload's content-type isn't in the bucket's `allowed_mime_types` allowlist (415, code `MIME_NOT_ALLOWED`). |
516
605
 
517
606
  ```ts
518
607
  import { KoolbaseConflictError, KoolbaseDataError } from '@techfinityedge/koolbase-react-native';
519
608
 
520
609
  try {
521
- await koolbase.db.upsert('users', { email }, { name });
610
+ await Koolbase.db.upsert('users', { email }, { name });
522
611
  } catch (e) {
523
612
  if (e instanceof KoolbaseConflictError) {
524
613
  showError(`That ${e.field ?? 'value'} is already taken.`);
@@ -533,13 +622,52 @@ try {
533
622
  > the background, so their conflicts surface via the sync engine, not as a
534
623
  > thrown error.
535
624
 
625
+ ### Storage errors
626
+
627
+ All storage failures extend `KoolbaseStorageError` (which extends `Error`):
628
+
629
+ | Error | When |
630
+ |---|---|
631
+ | `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` — the colliding path. |
632
+ | `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
633
+ | `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
634
+ | `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
635
+
636
+ ```ts
637
+ import {
638
+ KoolbaseStorageConflictError,
639
+ KoolbaseStorageError,
640
+ KoolbaseStoragePermissionError,
641
+ } from '@techfinityedge/koolbase-react-native';
642
+
643
+ try {
644
+ await Koolbase.storage.upload({
645
+ bucket: 'avatars',
646
+ path: 'me.png',
647
+ file: { uri, name: 'me.png', type: 'image/png' },
648
+ });
649
+ } catch (e) {
650
+ if (e instanceof KoolbaseStorageConflictError) {
651
+ // Already exists — prompt user to confirm overwrite
652
+ promptOverwrite(e.path);
653
+ } else if (e instanceof KoolbaseStoragePermissionError) {
654
+ showError('You do not have permission to upload here.');
655
+ } else if (e instanceof KoolbaseStorageError) {
656
+ // Catch-all for any other storage error
657
+ showError(e.message);
658
+ } else {
659
+ throw e;
660
+ }
661
+ }
662
+ ```
663
+
536
664
  ---
537
665
 
538
666
  ## What's included
539
667
 
540
668
  - Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
541
669
  - Database with offline-first cache, realtime subscriptions, and populate
542
- - Storage with download URLs
670
+ - Storage with presigned uploads and downloads, safe-by-default conflict handling
543
671
  - Realtime subscriptions over WebSocket
544
672
  - Authenticated functions (`ctx.auth` exposes the caller automatically)
545
673
  - Feature flags and remote config
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ import { KoolbaseConfig, VersionCheckResult } from './types';
19
19
  export * from './types';
20
20
  export * from './auth-errors';
21
21
  export * from './database-errors';
22
+ export * from './storage-errors';
22
23
  export { KoolbaseAuth, KoolbaseDatabase, KoolbaseFlags, KoolbaseFunctions, KoolbaseRealtime, KoolbaseStorage };
23
24
  export declare const Koolbase: {
24
25
  initialize(config: KoolbaseConfig): Promise<void>;
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ Object.defineProperty(exports, "KoolbaseStorage", { enumerable: true, get: funct
46
46
  __exportStar(require("./types"), exports);
47
47
  __exportStar(require("./auth-errors"), exports);
48
48
  __exportStar(require("./database-errors"), exports);
49
+ __exportStar(require("./storage-errors"), exports);
49
50
  let _auth = null;
50
51
  let _db = null;
51
52
  let _storage = null;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Base error type for all Koolbase storage errors. Catchable via
3
+ * `instanceof KoolbaseStorageError` to handle any storage-related failure
4
+ * generically; subclasses let you handle specific cases.
5
+ */
6
+ export declare class KoolbaseStorageError extends Error {
7
+ code?: string;
8
+ constructor(message: string, code?: string);
9
+ }
10
+ /**
11
+ * Thrown when an upload is rejected because an object already exists at
12
+ * the requested path — the server responds with 409 Conflict and code
13
+ * `PATH_CONFLICT`. Catch it to give the user an "overwrite this file?"
14
+ * prompt, then retry the upload with `overwrite: true`.
15
+ *
16
+ * `path` is the colliding path the server rejected, surfaced from the
17
+ * response body for diagnostics and UI.
18
+ *
19
+ * @example
20
+ * try {
21
+ * await Koolbase.storage.upload({
22
+ * bucket: 'avatars',
23
+ * path: 'me.png',
24
+ * file: { uri, name, type: 'image/png' },
25
+ * });
26
+ * } catch (e) {
27
+ * if (e instanceof KoolbaseStorageConflictError) {
28
+ * const ok = await confirm(`${e.path} already exists. Overwrite?`);
29
+ * if (ok) {
30
+ * await Koolbase.storage.upload({
31
+ * bucket: 'avatars',
32
+ * path: 'me.png',
33
+ * file: { uri, name, type: 'image/png' },
34
+ * overwrite: true,
35
+ * });
36
+ * }
37
+ * }
38
+ * }
39
+ */
40
+ export declare class KoolbaseStorageConflictError extends KoolbaseStorageError {
41
+ path?: string;
42
+ constructor(message?: string, path?: string);
43
+ }
44
+ /**
45
+ * Thrown when the requested bucket or object does not exist — the server
46
+ * responds with 404. Also surfaced for cross-tenant access attempts
47
+ * (Koolbase's 404-over-403 convention prevents enumeration in
48
+ * multi-tenant contexts).
49
+ */
50
+ export declare class KoolbaseStorageNotFoundError extends KoolbaseStorageError {
51
+ constructor(message?: string);
52
+ }
53
+ /**
54
+ * Thrown when the request is rejected as invalid — the server responds
55
+ * with 400 (e.g. a malformed path, missing field, invalid bucket name).
56
+ */
57
+ export declare class KoolbaseStorageValidationError extends KoolbaseStorageError {
58
+ constructor(message?: string);
59
+ }
60
+ /**
61
+ * Thrown when the caller is authenticated but not allowed to perform the
62
+ * storage operation — the server responds with 403.
63
+ */
64
+ export declare class KoolbaseStoragePermissionError extends KoolbaseStorageError {
65
+ constructor(message?: string);
66
+ }
67
+ /**
68
+ * Thrown when an upload would push the bucket past its configured
69
+ * `max_size_bytes` quota — the server responds with 409 Conflict and code
70
+ * `QUOTA_EXCEEDED`. The server cleans up the underlying R2 object before
71
+ * returning; nothing leaks. Catch this to surface a "bucket is full"
72
+ * message or prompt the caller to delete older files. The per-bucket
73
+ * quota is set at bucket creation time and is currently immutable.
74
+ *
75
+ * Distinct from {@link KoolbaseStorageConflictError} (which also uses
76
+ * 409 but means "path collides"); branch on the error type via
77
+ * `instanceof`, not on status.
78
+ */
79
+ export declare class KoolbaseStorageQuotaError extends KoolbaseStorageError {
80
+ constructor(message?: string);
81
+ }
82
+ /**
83
+ * Thrown when a single file exceeds the bucket's configured
84
+ * `max_file_size_bytes` — the server responds with 413 Payload Too Large
85
+ * and code `FILE_TOO_LARGE`. The server cleans up the underlying R2
86
+ * object before returning. The configured per-file limit lives on the
87
+ * bucket record; check `Bucket.maxFileSizeBytes` to surface a clear
88
+ * "files must be under X MB" message at the call site.
89
+ */
90
+ export declare class KoolbaseStorageFileTooLargeError extends KoolbaseStorageError {
91
+ constructor(message?: string);
92
+ }
93
+ /**
94
+ * Thrown when an upload's content-type isn't in the bucket's configured
95
+ * `allowed_mime_types` allowlist — the server responds with 415
96
+ * Unsupported Media Type and code `MIME_NOT_ALLOWED`. The check runs at
97
+ * presign time, so no bytes are transferred before rejection.
98
+ *
99
+ * Allowlists support `type/*` wildcards (e.g. `image/*` matches every
100
+ * image content-type). A bucket with no allowlist configured accepts
101
+ * every type.
102
+ */
103
+ export declare class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
104
+ constructor(message?: string);
105
+ }
106
+ /**
107
+ * Maps a non-2xx storage-layer response to a typed
108
+ * {@link KoolbaseStorageError}, preferring the server's stable `code` and
109
+ * falling back to the HTTP status for older or uncoded responses. Always
110
+ * returns an error to throw.
111
+ *
112
+ * Status-fallback note: HTTP 409 covers both PATH_CONFLICT and
113
+ * QUOTA_EXCEEDED. Without a `code` field, the mapper defaults 409 to
114
+ * {@link KoolbaseStorageConflictError} since path collisions are the more
115
+ * common case. Modern Koolbase servers always emit `code`, so this only
116
+ * matters for very old API responses or non-Koolbase 409s.
117
+ */
118
+ export declare function koolbaseStorageError(status: number, body: any, fallbackMessage?: string): KoolbaseStorageError;
119
+ /**
120
+ * Convenience wrapper over {@link koolbaseStorageError} that decodes the
121
+ * response body for you. Use at call sites that have the raw `Response`.
122
+ */
123
+ export declare function koolbaseStorageErrorFromResponse(res: Response, fallbackMessage?: string): Promise<KoolbaseStorageError>;
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageError = void 0;
4
+ exports.koolbaseStorageError = koolbaseStorageError;
5
+ exports.koolbaseStorageErrorFromResponse = koolbaseStorageErrorFromResponse;
6
+ /**
7
+ * Base error type for all Koolbase storage errors. Catchable via
8
+ * `instanceof KoolbaseStorageError` to handle any storage-related failure
9
+ * generically; subclasses let you handle specific cases.
10
+ */
11
+ class KoolbaseStorageError extends Error {
12
+ constructor(message, code) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = 'KoolbaseStorageError';
16
+ Object.setPrototypeOf(this, KoolbaseStorageError.prototype);
17
+ }
18
+ }
19
+ exports.KoolbaseStorageError = KoolbaseStorageError;
20
+ /**
21
+ * Thrown when an upload is rejected because an object already exists at
22
+ * the requested path — the server responds with 409 Conflict and code
23
+ * `PATH_CONFLICT`. Catch it to give the user an "overwrite this file?"
24
+ * prompt, then retry the upload with `overwrite: true`.
25
+ *
26
+ * `path` is the colliding path the server rejected, surfaced from the
27
+ * response body for diagnostics and UI.
28
+ *
29
+ * @example
30
+ * try {
31
+ * await Koolbase.storage.upload({
32
+ * bucket: 'avatars',
33
+ * path: 'me.png',
34
+ * file: { uri, name, type: 'image/png' },
35
+ * });
36
+ * } catch (e) {
37
+ * if (e instanceof KoolbaseStorageConflictError) {
38
+ * const ok = await confirm(`${e.path} already exists. Overwrite?`);
39
+ * if (ok) {
40
+ * await Koolbase.storage.upload({
41
+ * bucket: 'avatars',
42
+ * path: 'me.png',
43
+ * file: { uri, name, type: 'image/png' },
44
+ * overwrite: true,
45
+ * });
46
+ * }
47
+ * }
48
+ * }
49
+ */
50
+ class KoolbaseStorageConflictError extends KoolbaseStorageError {
51
+ constructor(message, path) {
52
+ super(message ?? 'An object already exists at this path', 'PATH_CONFLICT');
53
+ this.path = path;
54
+ this.name = 'KoolbaseStorageConflictError';
55
+ Object.setPrototypeOf(this, KoolbaseStorageConflictError.prototype);
56
+ }
57
+ }
58
+ exports.KoolbaseStorageConflictError = KoolbaseStorageConflictError;
59
+ /**
60
+ * Thrown when the requested bucket or object does not exist — the server
61
+ * responds with 404. Also surfaced for cross-tenant access attempts
62
+ * (Koolbase's 404-over-403 convention prevents enumeration in
63
+ * multi-tenant contexts).
64
+ */
65
+ class KoolbaseStorageNotFoundError extends KoolbaseStorageError {
66
+ constructor(message) {
67
+ super(message ?? 'The requested bucket or object was not found', 'not_found');
68
+ this.name = 'KoolbaseStorageNotFoundError';
69
+ Object.setPrototypeOf(this, KoolbaseStorageNotFoundError.prototype);
70
+ }
71
+ }
72
+ exports.KoolbaseStorageNotFoundError = KoolbaseStorageNotFoundError;
73
+ /**
74
+ * Thrown when the request is rejected as invalid — the server responds
75
+ * with 400 (e.g. a malformed path, missing field, invalid bucket name).
76
+ */
77
+ class KoolbaseStorageValidationError extends KoolbaseStorageError {
78
+ constructor(message) {
79
+ super(message ?? 'The storage request was invalid', 'validation_error');
80
+ this.name = 'KoolbaseStorageValidationError';
81
+ Object.setPrototypeOf(this, KoolbaseStorageValidationError.prototype);
82
+ }
83
+ }
84
+ exports.KoolbaseStorageValidationError = KoolbaseStorageValidationError;
85
+ /**
86
+ * Thrown when the caller is authenticated but not allowed to perform the
87
+ * storage operation — the server responds with 403.
88
+ */
89
+ class KoolbaseStoragePermissionError extends KoolbaseStorageError {
90
+ constructor(message) {
91
+ super(message ?? 'You do not have permission to perform this storage action', 'permission_denied');
92
+ this.name = 'KoolbaseStoragePermissionError';
93
+ Object.setPrototypeOf(this, KoolbaseStoragePermissionError.prototype);
94
+ }
95
+ }
96
+ exports.KoolbaseStoragePermissionError = KoolbaseStoragePermissionError;
97
+ /**
98
+ * Thrown when an upload would push the bucket past its configured
99
+ * `max_size_bytes` quota — the server responds with 409 Conflict and code
100
+ * `QUOTA_EXCEEDED`. The server cleans up the underlying R2 object before
101
+ * returning; nothing leaks. Catch this to surface a "bucket is full"
102
+ * message or prompt the caller to delete older files. The per-bucket
103
+ * quota is set at bucket creation time and is currently immutable.
104
+ *
105
+ * Distinct from {@link KoolbaseStorageConflictError} (which also uses
106
+ * 409 but means "path collides"); branch on the error type via
107
+ * `instanceof`, not on status.
108
+ */
109
+ class KoolbaseStorageQuotaError extends KoolbaseStorageError {
110
+ constructor(message) {
111
+ super(message ?? 'Bucket quota exceeded', 'QUOTA_EXCEEDED');
112
+ this.name = 'KoolbaseStorageQuotaError';
113
+ Object.setPrototypeOf(this, KoolbaseStorageQuotaError.prototype);
114
+ }
115
+ }
116
+ exports.KoolbaseStorageQuotaError = KoolbaseStorageQuotaError;
117
+ /**
118
+ * Thrown when a single file exceeds the bucket's configured
119
+ * `max_file_size_bytes` — the server responds with 413 Payload Too Large
120
+ * and code `FILE_TOO_LARGE`. The server cleans up the underlying R2
121
+ * object before returning. The configured per-file limit lives on the
122
+ * bucket record; check `Bucket.maxFileSizeBytes` to surface a clear
123
+ * "files must be under X MB" message at the call site.
124
+ */
125
+ class KoolbaseStorageFileTooLargeError extends KoolbaseStorageError {
126
+ constructor(message) {
127
+ super(message ?? 'File exceeds the bucket maximum file size', 'FILE_TOO_LARGE');
128
+ this.name = 'KoolbaseStorageFileTooLargeError';
129
+ Object.setPrototypeOf(this, KoolbaseStorageFileTooLargeError.prototype);
130
+ }
131
+ }
132
+ exports.KoolbaseStorageFileTooLargeError = KoolbaseStorageFileTooLargeError;
133
+ /**
134
+ * Thrown when an upload's content-type isn't in the bucket's configured
135
+ * `allowed_mime_types` allowlist — the server responds with 415
136
+ * Unsupported Media Type and code `MIME_NOT_ALLOWED`. The check runs at
137
+ * presign time, so no bytes are transferred before rejection.
138
+ *
139
+ * Allowlists support `type/*` wildcards (e.g. `image/*` matches every
140
+ * image content-type). A bucket with no allowlist configured accepts
141
+ * every type.
142
+ */
143
+ class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
144
+ constructor(message) {
145
+ super(message ?? 'Content-type not allowed for this bucket', 'MIME_NOT_ALLOWED');
146
+ this.name = 'KoolbaseStorageMimeTypeError';
147
+ Object.setPrototypeOf(this, KoolbaseStorageMimeTypeError.prototype);
148
+ }
149
+ }
150
+ exports.KoolbaseStorageMimeTypeError = KoolbaseStorageMimeTypeError;
151
+ /**
152
+ * Maps a non-2xx storage-layer response to a typed
153
+ * {@link KoolbaseStorageError}, preferring the server's stable `code` and
154
+ * falling back to the HTTP status for older or uncoded responses. Always
155
+ * returns an error to throw.
156
+ *
157
+ * Status-fallback note: HTTP 409 covers both PATH_CONFLICT and
158
+ * QUOTA_EXCEEDED. Without a `code` field, the mapper defaults 409 to
159
+ * {@link KoolbaseStorageConflictError} since path collisions are the more
160
+ * common case. Modern Koolbase servers always emit `code`, so this only
161
+ * matters for very old API responses or non-Koolbase 409s.
162
+ */
163
+ function koolbaseStorageError(status, body, fallbackMessage = 'Storage request failed') {
164
+ const code = body?.code;
165
+ const message = body?.error ?? fallbackMessage;
166
+ // ─── code-first ───
167
+ switch (code) {
168
+ case 'PATH_CONFLICT':
169
+ return new KoolbaseStorageConflictError(message, body?.path);
170
+ case 'QUOTA_EXCEEDED':
171
+ return new KoolbaseStorageQuotaError(message);
172
+ case 'FILE_TOO_LARGE':
173
+ return new KoolbaseStorageFileTooLargeError(message);
174
+ case 'MIME_NOT_ALLOWED':
175
+ return new KoolbaseStorageMimeTypeError(message);
176
+ }
177
+ // ─── status fallback (pre-code servers or uncoded paths) ───
178
+ switch (status) {
179
+ case 409:
180
+ return new KoolbaseStorageConflictError(message);
181
+ case 413:
182
+ return new KoolbaseStorageFileTooLargeError(message);
183
+ case 415:
184
+ return new KoolbaseStorageMimeTypeError(message);
185
+ case 404:
186
+ return new KoolbaseStorageNotFoundError(message);
187
+ case 403:
188
+ return new KoolbaseStoragePermissionError(message);
189
+ case 400:
190
+ return new KoolbaseStorageValidationError(message);
191
+ }
192
+ return new KoolbaseStorageError(message, code);
193
+ }
194
+ /**
195
+ * Convenience wrapper over {@link koolbaseStorageError} that decodes the
196
+ * response body for you. Use at call sites that have the raw `Response`.
197
+ */
198
+ async function koolbaseStorageErrorFromResponse(res, fallbackMessage = 'Storage request failed') {
199
+ let body = {};
200
+ try {
201
+ body = await res.json();
202
+ }
203
+ catch (_) {
204
+ // body wasn't JSON — fall through with empty object
205
+ }
206
+ return koolbaseStorageError(res.status, body, fallbackMessage);
207
+ }
package/dist/storage.d.ts CHANGED
@@ -1,12 +1,39 @@
1
- import { KoolbaseConfig, UploadOptions } from './types';
1
+ import { KoolbaseConfig, UploadOptions, UploadResult } from './types';
2
+ /**
3
+ * Koolbase storage client — uploads, downloads, and deletes via presigned
4
+ * Cloudflare R2 URLs.
5
+ *
6
+ * Uploads are **safe-by-default** (v5+): an upload to a path where an object
7
+ * already exists is rejected with {@link KoolbaseStorageConflictError} unless
8
+ * `overwrite: true` is passed.
9
+ */
2
10
  export declare class KoolbaseStorage {
3
11
  private config;
4
12
  private getToken;
5
13
  constructor(config: KoolbaseConfig, getToken: () => Promise<string | null>);
6
14
  private buildHeaders;
7
- upload(options: UploadOptions): Promise<{
8
- url: string;
9
- }>;
15
+ /**
16
+ * Upload a file to a bucket. Returns the object metadata and a download URL.
17
+ *
18
+ * By default (`overwrite: false`), uploads to a path where an object
19
+ * already exists are **rejected** with a {@link KoolbaseStorageConflictError}.
20
+ * Catch it to prompt the user, then retry with `overwrite: true` to replace
21
+ * the existing object — or with a different `path`.
22
+ *
23
+ * Set `overwrite: true` for true upsert semantics — silently replace any
24
+ * existing object at this path.
25
+ *
26
+ * **Breaking change in v5.0.0**: the default flipped from silent overwrite
27
+ * (legacy behavior) to safe-by-default. If you previously relied on uploads
28
+ * overwriting silently, pass `overwrite: true` explicitly.
29
+ */
30
+ upload(options: UploadOptions): Promise<UploadResult>;
31
+ /**
32
+ * Get a signed download URL for a file.
33
+ */
10
34
  getDownloadUrl(bucket: string, path: string): Promise<string>;
35
+ /**
36
+ * Delete a file from a bucket.
37
+ */
11
38
  delete(bucket: string, path: string): Promise<void>;
12
39
  }
package/dist/storage.js CHANGED
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KoolbaseStorage = void 0;
4
+ const storage_errors_1 = require("./storage-errors");
5
+ /**
6
+ * Koolbase storage client — uploads, downloads, and deletes via presigned
7
+ * Cloudflare R2 URLs.
8
+ *
9
+ * Uploads are **safe-by-default** (v5+): an upload to a path where an object
10
+ * already exists is rejected with {@link KoolbaseStorageConflictError} unless
11
+ * `overwrite: true` is passed.
12
+ */
4
13
  class KoolbaseStorage {
5
14
  constructor(config, getToken) {
6
15
  this.config = config;
@@ -13,50 +22,130 @@ class KoolbaseStorage {
13
22
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
14
23
  };
15
24
  }
25
+ /**
26
+ * Upload a file to a bucket. Returns the object metadata and a download URL.
27
+ *
28
+ * By default (`overwrite: false`), uploads to a path where an object
29
+ * already exists are **rejected** with a {@link KoolbaseStorageConflictError}.
30
+ * Catch it to prompt the user, then retry with `overwrite: true` to replace
31
+ * the existing object — or with a different `path`.
32
+ *
33
+ * Set `overwrite: true` for true upsert semantics — silently replace any
34
+ * existing object at this path.
35
+ *
36
+ * **Breaking change in v5.0.0**: the default flipped from silent overwrite
37
+ * (legacy behavior) to safe-by-default. If you previously relied on uploads
38
+ * overwriting silently, pass `overwrite: true` explicitly.
39
+ */
16
40
  async upload(options) {
17
- // Get presigned upload URL
18
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/${options.bucket}/upload`, {
41
+ const overwrite = options.overwrite ?? false;
42
+ const contentType = options.file.type;
43
+ // ─── Step 1: Get presigned upload URL ───
44
+ const urlRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
19
45
  method: 'POST',
20
- headers: { ...(await this.buildHeaders()), 'Content-Type': 'application/json' },
46
+ headers: {
47
+ ...(await this.buildHeaders()),
48
+ 'Content-Type': 'application/json',
49
+ },
21
50
  body: JSON.stringify({
51
+ bucket: options.bucket,
22
52
  path: options.path,
23
- content_type: options.file.type,
53
+ content_type: contentType,
54
+ overwrite,
24
55
  }),
25
56
  });
26
- if (!res.ok) {
27
- const data = await res.json();
28
- throw new Error(data.error ?? 'Failed to get upload URL');
57
+ if (!urlRes.ok) {
58
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(urlRes, 'Failed to get upload URL');
59
+ }
60
+ const { upload_url } = (await urlRes.json());
61
+ // ─── Step 2: Upload directly to R2 ───
62
+ // RN's fetch resolves local file URIs and Blob bodies on a raw PUT.
63
+ // R2 presigned URLs expect raw binary, NOT multipart/form-data.
64
+ const fileResp = await fetch(options.file.uri);
65
+ const fileBlob = await fileResp.blob();
66
+ const fileSize = fileBlob.size;
67
+ const uploadRes = await fetch(upload_url, {
68
+ method: 'PUT',
69
+ headers: { 'Content-Type': contentType },
70
+ body: fileBlob,
71
+ });
72
+ if (!uploadRes.ok) {
73
+ // R2 PUT errors don't follow the Koolbase error shape — surface as a
74
+ // generic storage error rather than trying to decode a Koolbase body.
75
+ throw new storage_errors_1.KoolbaseStorageError(`Upload to storage failed: ${uploadRes.status}`);
29
76
  }
30
- const { upload_url, public_url } = await res.json();
31
- // Upload to presigned URL
32
- const formData = new FormData();
33
- formData.append('file', {
34
- uri: options.file.uri,
35
- name: options.file.name,
36
- type: options.file.type,
77
+ const etag = uploadRes.headers.get('etag') ?? '';
78
+ // ─── Step 3: Confirm upload ───
79
+ const confirmRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
80
+ method: 'POST',
81
+ headers: {
82
+ ...(await this.buildHeaders()),
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify({
86
+ bucket: options.bucket,
87
+ path: options.path,
88
+ size: fileSize,
89
+ content_type: contentType,
90
+ etag,
91
+ overwrite,
92
+ }),
37
93
  });
38
- await fetch(upload_url, { method: 'PUT', body: formData });
39
- return { url: public_url };
94
+ if (!confirmRes.ok) {
95
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(confirmRes, 'Failed to confirm upload');
96
+ }
97
+ const raw = await confirmRes.json();
98
+ const object = mapObjectFromServer(raw);
99
+ // ─── Step 4: Get download URL ───
100
+ const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
101
+ return { object, downloadUrl };
40
102
  }
103
+ /**
104
+ * Get a signed download URL for a file.
105
+ */
41
106
  async getDownloadUrl(bucket, path) {
42
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/${bucket}/download?path=${encodeURIComponent(path)}`, { headers: await this.buildHeaders() });
107
+ const url = `${this.config.baseUrl}/v1/sdk/storage/download-url` +
108
+ `?bucket=${encodeURIComponent(bucket)}&path=${encodeURIComponent(path)}`;
109
+ const res = await fetch(url, { headers: await this.buildHeaders() });
43
110
  if (!res.ok) {
44
- const data = await res.json();
45
- throw new Error(data.error ?? 'Failed to get download URL');
111
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to get download URL');
46
112
  }
47
- const { url } = await res.json();
48
- return url;
113
+ const data = (await res.json());
114
+ return data.url;
49
115
  }
116
+ /**
117
+ * Delete a file from a bucket.
118
+ */
50
119
  async delete(bucket, path) {
51
- const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/${bucket}/delete`, {
120
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/object`, {
52
121
  method: 'DELETE',
53
- headers: { ...(await this.buildHeaders()), 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ path }),
122
+ headers: {
123
+ ...(await this.buildHeaders()),
124
+ 'Content-Type': 'application/json',
125
+ },
126
+ body: JSON.stringify({ bucket, path }),
55
127
  });
56
- if (!res.ok && res.status !== 204) {
57
- const data = await res.json();
58
- throw new Error(data.error ?? 'Delete failed');
128
+ if (res.status === 204)
129
+ return;
130
+ if (!res.ok) {
131
+ throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to delete file');
59
132
  }
60
133
  }
61
134
  }
62
135
  exports.KoolbaseStorage = KoolbaseStorage;
136
+ /**
137
+ * Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
138
+ */
139
+ function mapObjectFromServer(raw) {
140
+ return {
141
+ id: raw.id,
142
+ projectId: raw.project_id,
143
+ bucketId: raw.bucket_id,
144
+ userId: raw.user_id ?? null,
145
+ path: raw.path,
146
+ size: raw.size ?? 0,
147
+ contentType: raw.content_type ?? null,
148
+ createdAt: raw.created_at,
149
+ updatedAt: raw.updated_at,
150
+ };
151
+ }
package/dist/types.d.ts CHANGED
@@ -155,8 +155,38 @@ export interface UploadOptions {
155
155
  name: string;
156
156
  type: string;
157
157
  };
158
+ /**
159
+ * If `false` (default in v5+), an upload to a path where an object
160
+ * already exists is rejected with `KoolbaseStorageConflictError`. Pass
161
+ * `true` to silently replace the existing object.
162
+ */
163
+ overwrite?: boolean;
158
164
  onProgress?: (percent: number) => void;
159
165
  }
166
+ /**
167
+ * A stored object's server-side metadata. Field names are camelCase here
168
+ * even though the wire format is snake_case — the SDK maps for you.
169
+ */
170
+ export interface KoolbaseObject {
171
+ id: string;
172
+ projectId: string;
173
+ bucketId: string;
174
+ userId: string | null;
175
+ path: string;
176
+ size: number;
177
+ contentType: string | null;
178
+ /** ISO 8601 timestamp from the server. */
179
+ createdAt: string;
180
+ /** ISO 8601 timestamp from the server. */
181
+ updatedAt: string;
182
+ }
183
+ /**
184
+ * Result of a successful `KoolbaseStorage.upload()` call.
185
+ */
186
+ export interface UploadResult {
187
+ object: KoolbaseObject;
188
+ downloadUrl: string;
189
+ }
160
190
  export interface RealtimeEvent {
161
191
  type: 'created' | 'updated' | 'deleted';
162
192
  collection: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "4.2.1",
3
+ "version": "5.1.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",