@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 +169 -41
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/storage-errors.d.ts +123 -0
- package/dist/storage-errors.js +207 -0
- package/dist/storage.d.ts +31 -4
- package/dist/storage.js +116 -27
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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({
|
|
128
|
+
await Koolbase.auth.sendOtp({ phoneNumber: '+233200000000' });
|
|
129
129
|
|
|
130
130
|
// Verify and sign in
|
|
131
131
|
await Koolbase.auth.verifyOtp({
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
+
A write that would violate a unique constraint throws `KoolbaseConflictError`:
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
512
|
-
| `
|
|
513
|
-
| `
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
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
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
18
|
-
const
|
|
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: {
|
|
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:
|
|
53
|
+
content_type: contentType,
|
|
54
|
+
overwrite,
|
|
24
55
|
}),
|
|
25
56
|
});
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
120
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/object`, {
|
|
52
121
|
method: 'DELETE',
|
|
53
|
-
headers: {
|
|
54
|
-
|
|
122
|
+
headers: {
|
|
123
|
+
...(await this.buildHeaders()),
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({ bucket, path }),
|
|
55
127
|
});
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
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": "
|
|
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",
|