@techfinityedge/koolbase-react-native 4.2.0 → 5.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 +132 -46
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/realtime.js +12 -7
- package/dist/storage-errors.d.ts +78 -0
- package/dist/storage-errors.js +137 -0
- package/dist/storage.d.ts +31 -4
- package/dist/storage.js +116 -27
- package/dist/types.d.ts +32 -1
- 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,42 +281,89 @@ 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({
|
|
292
|
+
bucket: 'avatars',
|
|
293
|
+
path: `user-${userId}.jpg`,
|
|
294
|
+
file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Upload — silently replaces any existing object at this path
|
|
298
|
+
await Koolbase.storage.upload({
|
|
286
299
|
bucket: 'avatars',
|
|
287
300
|
path: `user-${userId}.jpg`,
|
|
288
301
|
file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
|
|
302
|
+
overwrite: true,
|
|
289
303
|
});
|
|
290
304
|
|
|
291
|
-
|
|
305
|
+
// Get download URL
|
|
306
|
+
const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg`);
|
|
292
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) {
|
|
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
|
+
|
|
296
344
|
---
|
|
297
345
|
|
|
298
346
|
## Realtime
|
|
299
347
|
|
|
300
|
-
Subscribe to live changes on a collection.
|
|
301
|
-
|
|
348
|
+
Subscribe to live changes on a collection. Uses the signed-in user's session, so
|
|
349
|
+
subscribe after login. Streams `created`, `updated`, and `deleted` events for
|
|
302
350
|
collections whose read rule is `public` or `authenticated`.
|
|
303
351
|
|
|
304
352
|
```ts
|
|
305
|
-
import { Koolbase } from '@techfinityedge/koolbase-react-native';
|
|
306
|
-
|
|
307
353
|
const unsubscribe = Koolbase.realtime.subscribe('messages', (event) => {
|
|
308
|
-
// event.type
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
354
|
+
// event.type -> 'created' | 'updated' | 'deleted'
|
|
355
|
+
if (event.type === 'deleted') {
|
|
356
|
+
console.log('deleted', event.recordId); // recordId on deletes
|
|
357
|
+
} else {
|
|
358
|
+
console.log(event.type, event.record!.data); // record on created/updated
|
|
359
|
+
}
|
|
312
360
|
});
|
|
313
361
|
|
|
314
|
-
unsubscribe();
|
|
362
|
+
unsubscribe();
|
|
315
363
|
```
|
|
316
364
|
|
|
317
|
-
The socket opens lazily
|
|
318
|
-
|
|
319
|
-
don't pass it.
|
|
365
|
+
The socket opens lazily, is shared, and reconnects automatically. The project is
|
|
366
|
+
taken from the user's session.
|
|
320
367
|
|
|
321
368
|
---
|
|
322
369
|
|
|
@@ -519,7 +566,7 @@ All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
|
|
|
519
566
|
import { KoolbaseConflictError, KoolbaseDataError } from '@techfinityedge/koolbase-react-native';
|
|
520
567
|
|
|
521
568
|
try {
|
|
522
|
-
await
|
|
569
|
+
await Koolbase.db.upsert('users', { email }, { name });
|
|
523
570
|
} catch (e) {
|
|
524
571
|
if (e instanceof KoolbaseConflictError) {
|
|
525
572
|
showError(`That ${e.field ?? 'value'} is already taken.`);
|
|
@@ -534,13 +581,52 @@ try {
|
|
|
534
581
|
> the background, so their conflicts surface via the sync engine, not as a
|
|
535
582
|
> thrown error.
|
|
536
583
|
|
|
584
|
+
### Storage errors
|
|
585
|
+
|
|
586
|
+
All storage failures extend `KoolbaseStorageError` (which extends `Error`):
|
|
587
|
+
|
|
588
|
+
| Error | When |
|
|
589
|
+
|---|---|
|
|
590
|
+
| `KoolbaseStorageConflictError` | An upload targets a path that's already taken and `overwrite: false` (409, code `PATH_CONFLICT`). Exposes `.path` — the colliding path. |
|
|
591
|
+
| `KoolbaseStorageNotFoundError` | The bucket or object doesn't exist (404). |
|
|
592
|
+
| `KoolbaseStorageValidationError` | The request was rejected as invalid — bad path, missing field (400). |
|
|
593
|
+
| `KoolbaseStoragePermissionError` | The caller is not allowed to perform the operation (403). |
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
import {
|
|
597
|
+
KoolbaseStorageConflictError,
|
|
598
|
+
KoolbaseStorageError,
|
|
599
|
+
KoolbaseStoragePermissionError,
|
|
600
|
+
} from '@techfinityedge/koolbase-react-native';
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
await Koolbase.storage.upload({
|
|
604
|
+
bucket: 'avatars',
|
|
605
|
+
path: 'me.png',
|
|
606
|
+
file: { uri, name: 'me.png', type: 'image/png' },
|
|
607
|
+
});
|
|
608
|
+
} catch (e) {
|
|
609
|
+
if (e instanceof KoolbaseStorageConflictError) {
|
|
610
|
+
// Already exists — prompt user to confirm overwrite
|
|
611
|
+
promptOverwrite(e.path);
|
|
612
|
+
} else if (e instanceof KoolbaseStoragePermissionError) {
|
|
613
|
+
showError('You do not have permission to upload here.');
|
|
614
|
+
} else if (e instanceof KoolbaseStorageError) {
|
|
615
|
+
// Catch-all for any other storage error
|
|
616
|
+
showError(e.message);
|
|
617
|
+
} else {
|
|
618
|
+
throw e;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
537
623
|
---
|
|
538
624
|
|
|
539
625
|
## What's included
|
|
540
626
|
|
|
541
627
|
- Authentication: email + password, Apple Sign-In, Google Sign-In, phone + OTP
|
|
542
628
|
- Database with offline-first cache, realtime subscriptions, and populate
|
|
543
|
-
- Storage with
|
|
629
|
+
- Storage with presigned uploads and downloads, safe-by-default conflict handling
|
|
544
630
|
- Realtime subscriptions over WebSocket
|
|
545
631
|
- Authenticated functions (`ctx.auth` exposes the caller automatically)
|
|
546
632
|
- 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;
|
package/dist/realtime.js
CHANGED
|
@@ -94,13 +94,18 @@ class KoolbaseRealtime {
|
|
|
94
94
|
if (!mapped)
|
|
95
95
|
return; // ignore subscribed / unsubscribed / error / unknown
|
|
96
96
|
const payload = raw.payload;
|
|
97
|
-
if (!payload || !payload.collection
|
|
98
|
-
return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
collection: payload.collection,
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
if (!payload || !payload.collection)
|
|
98
|
+
return;
|
|
99
|
+
let msg;
|
|
100
|
+
if (mapped === 'deleted') {
|
|
101
|
+
msg = { type: 'deleted', collection: payload.collection, recordId: payload.record_id };
|
|
102
|
+
}
|
|
103
|
+
else if (payload.record) {
|
|
104
|
+
msg = { type: mapped, collection: payload.collection, record: (0, record_1.recordFromWire)(payload.record) };
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
104
109
|
(this.listeners.get(payload.collection) ?? []).forEach((cb) => cb(msg));
|
|
105
110
|
};
|
|
106
111
|
ws.onclose = () => {
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
* Maps a non-2xx storage-layer response to a typed
|
|
69
|
+
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
70
|
+
* falling back to the HTTP status for older or uncoded responses. Always
|
|
71
|
+
* returns an error to throw.
|
|
72
|
+
*/
|
|
73
|
+
export declare function koolbaseStorageError(status: number, body: any, fallbackMessage?: string): KoolbaseStorageError;
|
|
74
|
+
/**
|
|
75
|
+
* Convenience wrapper over {@link koolbaseStorageError} that decodes the
|
|
76
|
+
* response body for you. Use at call sites that have the raw `Response`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function koolbaseStorageErrorFromResponse(res: Response, fallbackMessage?: string): Promise<KoolbaseStorageError>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
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
|
+
* Maps a non-2xx storage-layer response to a typed
|
|
99
|
+
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
100
|
+
* falling back to the HTTP status for older or uncoded responses. Always
|
|
101
|
+
* returns an error to throw.
|
|
102
|
+
*/
|
|
103
|
+
function koolbaseStorageError(status, body, fallbackMessage = 'Storage request failed') {
|
|
104
|
+
const code = body?.code;
|
|
105
|
+
const message = body?.error ?? fallbackMessage;
|
|
106
|
+
// ─── code-first ───
|
|
107
|
+
switch (code) {
|
|
108
|
+
case 'PATH_CONFLICT':
|
|
109
|
+
return new KoolbaseStorageConflictError(message, body?.path);
|
|
110
|
+
}
|
|
111
|
+
// ─── status fallback (pre-code servers or uncoded paths) ───
|
|
112
|
+
switch (status) {
|
|
113
|
+
case 409:
|
|
114
|
+
return new KoolbaseStorageConflictError(message);
|
|
115
|
+
case 404:
|
|
116
|
+
return new KoolbaseStorageNotFoundError(message);
|
|
117
|
+
case 403:
|
|
118
|
+
return new KoolbaseStoragePermissionError(message);
|
|
119
|
+
case 400:
|
|
120
|
+
return new KoolbaseStorageValidationError(message);
|
|
121
|
+
}
|
|
122
|
+
return new KoolbaseStorageError(message, code);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Convenience wrapper over {@link koolbaseStorageError} that decodes the
|
|
126
|
+
* response body for you. Use at call sites that have the raw `Response`.
|
|
127
|
+
*/
|
|
128
|
+
async function koolbaseStorageErrorFromResponse(res, fallbackMessage = 'Storage request failed') {
|
|
129
|
+
let body = {};
|
|
130
|
+
try {
|
|
131
|
+
body = await res.json();
|
|
132
|
+
}
|
|
133
|
+
catch (_) {
|
|
134
|
+
// body wasn't JSON — fall through with empty object
|
|
135
|
+
}
|
|
136
|
+
return koolbaseStorageError(res.status, body, fallbackMessage);
|
|
137
|
+
}
|
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,12 +155,43 @@ 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;
|
|
163
|
-
record
|
|
193
|
+
record?: KoolbaseRecord;
|
|
194
|
+
recordId?: string;
|
|
164
195
|
}
|
|
165
196
|
export type RealtimeCallback = (event: RealtimeEvent) => void;
|
|
166
197
|
export interface BootstrapPayload {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techfinityedge/koolbase-react-native",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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",
|