@techfinityedge/koolbase-react-native 5.1.1 → 5.3.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 +54 -0
- package/dist/storage-errors.d.ts +40 -0
- package/dist/storage-errors.js +43 -1
- package/dist/storage.d.ts +76 -1
- package/dist/storage.js +115 -8
- package/dist/types.d.ts +32 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -73,6 +73,8 @@ const unsubscribe = Koolbase.auth.onAuthStateChange((user) => {
|
|
|
73
73
|
});
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
---
|
|
77
|
+
|
|
76
78
|
### OAuth — Apple
|
|
77
79
|
|
|
78
80
|
Apple Sign-In uses the native authentication flow via `@invertase/react-native-apple-authentication` as a peer dependency:
|
|
@@ -100,6 +102,8 @@ const session = await Koolbase.auth.signInWithApple({
|
|
|
100
102
|
|
|
101
103
|
Configure Apple Sign-In for your environment with your iOS app's Bundle ID. Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
|
|
102
104
|
|
|
105
|
+
---
|
|
106
|
+
|
|
103
107
|
### OAuth — Google
|
|
104
108
|
|
|
105
109
|
Google Sign-In uses the native authentication flow via `@react-native-google-signin/google-signin` as a peer dependency:
|
|
@@ -121,6 +125,8 @@ const session = await Koolbase.auth.signInWithGoogle({
|
|
|
121
125
|
|
|
122
126
|
Configure Google Sign-In for your environment with the OAuth client IDs from Google Cloud Console (typically one each for iOS, Android, and web). Full setup guide at [docs.koolbase.com/auth/oauth](https://docs.koolbase.com/auth/oauth).
|
|
123
127
|
|
|
128
|
+
---
|
|
129
|
+
|
|
124
130
|
### Phone + OTP
|
|
125
131
|
|
|
126
132
|
```typescript
|
|
@@ -173,6 +179,8 @@ await Koolbase.db.update('record-id', { title: 'Updated' });
|
|
|
173
179
|
await Koolbase.db.delete('record-id');
|
|
174
180
|
```
|
|
175
181
|
|
|
182
|
+
---
|
|
183
|
+
|
|
176
184
|
### Handling unique-constraint conflicts
|
|
177
185
|
|
|
178
186
|
A write that would violate a unique constraint throws `KoolbaseConflictError`:
|
|
@@ -187,6 +195,46 @@ try {
|
|
|
187
195
|
}
|
|
188
196
|
```
|
|
189
197
|
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Public bucket URLs
|
|
201
|
+
|
|
202
|
+
For files in public buckets, you can construct the stable CDN URL directly — no
|
|
203
|
+
network call, no expiry, embeddable anywhere a browser fetches a URL.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { KoolbaseStorage } from '@techfinityedge/koolbase-react-native';
|
|
207
|
+
|
|
208
|
+
// From a KoolbaseObject you already have (e.g. from upload() or another read)
|
|
209
|
+
const { object } = await Koolbase.storage.upload({
|
|
210
|
+
bucket: 'avatars',
|
|
211
|
+
path: `user-${userId}.jpg`,
|
|
212
|
+
file: { uri: imageUri, name: 'avatar.jpg', type: 'image/jpeg' },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const url = KoolbaseStorage.publicUrlForObject(object, 'avatars');
|
|
216
|
+
// url is null for private-bucket objects; the CDN URL for public-bucket ones.
|
|
217
|
+
|
|
218
|
+
if (url) {
|
|
219
|
+
// Safe to use — file lives in the public R2 bucket
|
|
220
|
+
return <Image source={{ uri: url }} />;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// For build-time URL construction (no Object on hand)
|
|
224
|
+
const url = KoolbaseStorage.publicUrl({
|
|
225
|
+
projectId: 'proj_abc',
|
|
226
|
+
bucket: 'avatars',
|
|
227
|
+
path: 'user-123.jpg',
|
|
228
|
+
});
|
|
229
|
+
// Always returns the URL pattern; caller is responsible for knowing
|
|
230
|
+
// the file lives in a public bucket. For files in private buckets,
|
|
231
|
+
// the resulting URL will 404.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
URLs follow the pattern `https://cdn.koolbase.com/{project_id}/{bucket}/{path}` — long-lived, edge-cached, no authentication. For files in private buckets, use `getDownloadUrl` instead, which returns a 1-hour presigned URL.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
190
238
|
### Upsert
|
|
191
239
|
|
|
192
240
|
Insert a record, or update the existing one matching a filter.
|
|
@@ -230,6 +278,8 @@ if (isFromCache) console.log('Served from local cache');
|
|
|
230
278
|
await Koolbase.db.syncPendingWrites();
|
|
231
279
|
```
|
|
232
280
|
|
|
281
|
+
---
|
|
282
|
+
|
|
233
283
|
### Atomic batch writes
|
|
234
284
|
|
|
235
285
|
Run multiple writes in a single server-side transaction. All operations commit together or none are applied — any failure rolls back the entire batch.
|
|
@@ -309,6 +359,8 @@ const url = await Koolbase.storage.getDownloadUrl('avatars', `user-${userId}.jpg
|
|
|
309
359
|
await Koolbase.storage.delete('avatars', `user-${userId}.jpg`);
|
|
310
360
|
```
|
|
311
361
|
|
|
362
|
+
---
|
|
363
|
+
|
|
312
364
|
### Handling upload conflicts
|
|
313
365
|
|
|
314
366
|
For user-supplied filenames, prompt the user before overwriting:
|
|
@@ -622,6 +674,8 @@ try {
|
|
|
622
674
|
> the background, so their conflicts surface via the sync engine, not as a
|
|
623
675
|
> thrown error.
|
|
624
676
|
|
|
677
|
+
---
|
|
678
|
+
|
|
625
679
|
### Storage errors
|
|
626
680
|
|
|
627
681
|
All storage failures extend `KoolbaseStorageError` (which extends `Error`):
|
package/dist/storage-errors.d.ts
CHANGED
|
@@ -103,6 +103,46 @@ export declare class KoolbaseStorageFileTooLargeError extends KoolbaseStorageErr
|
|
|
103
103
|
export declare class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
|
|
104
104
|
constructor(message?: string);
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Thrown when an object metadata payload (either at upload-confirm time
|
|
108
|
+
* or via `updateMetadata`) fails server-side validation — the server
|
|
109
|
+
* responds with 400 and code `metadata_invalid`.
|
|
110
|
+
*
|
|
111
|
+
* The `detail` field carries the specific reason from the server — e.g.
|
|
112
|
+
* `'key "foo bar": must match [a-z0-9_]+'`, `'exceeds 50 keys (got 53)'`,
|
|
113
|
+
* or `'exceeds 8192 bytes total (sum of all key + value lengths)'`. The
|
|
114
|
+
* detail names the failing key and rule so callers can fix the offending
|
|
115
|
+
* entry without guessing what shape rule was violated.
|
|
116
|
+
*
|
|
117
|
+
* Validation rules (enforced server-side):
|
|
118
|
+
* - At most 50 keys per object.
|
|
119
|
+
* - At most 8KB total (sum of byte lengths across all keys + values).
|
|
120
|
+
* - Keys: 1–64 chars, must match `[a-z0-9_]+`.
|
|
121
|
+
* - Keys with a leading underscore are reserved for system use.
|
|
122
|
+
* - Values: at most 1024 chars each.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* try {
|
|
126
|
+
* await Koolbase.storage.updateMetadata('photos', 'sunset.jpg', {
|
|
127
|
+
* tag: 'sunset',
|
|
128
|
+
* 'BAD KEY': 'oops',
|
|
129
|
+
* });
|
|
130
|
+
* } catch (e) {
|
|
131
|
+
* if (e instanceof KoolbaseStorageMetadataInvalidError) {
|
|
132
|
+
* console.warn('Metadata rejected:', e.detail);
|
|
133
|
+
* // -> 'Metadata rejected: key "BAD KEY": must match [a-z0-9_]+'
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
*/
|
|
137
|
+
export declare class KoolbaseStorageMetadataInvalidError extends KoolbaseStorageError {
|
|
138
|
+
/**
|
|
139
|
+
* The specific validation failure reported by the server. Names the
|
|
140
|
+
* failing key (when applicable) and the rule that was violated.
|
|
141
|
+
* Surface this directly to developer logs or user-facing UI.
|
|
142
|
+
*/
|
|
143
|
+
detail?: string;
|
|
144
|
+
constructor(message?: string, detail?: string);
|
|
145
|
+
}
|
|
106
146
|
/**
|
|
107
147
|
* Maps a non-2xx storage-layer response to a typed
|
|
108
148
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
package/dist/storage-errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
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;
|
|
3
|
+
exports.KoolbaseStorageMetadataInvalidError = exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageError = void 0;
|
|
4
4
|
exports.koolbaseStorageError = koolbaseStorageError;
|
|
5
5
|
exports.koolbaseStorageErrorFromResponse = koolbaseStorageErrorFromResponse;
|
|
6
6
|
/**
|
|
@@ -148,6 +148,46 @@ class KoolbaseStorageMimeTypeError extends KoolbaseStorageError {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
exports.KoolbaseStorageMimeTypeError = KoolbaseStorageMimeTypeError;
|
|
151
|
+
/**
|
|
152
|
+
* Thrown when an object metadata payload (either at upload-confirm time
|
|
153
|
+
* or via `updateMetadata`) fails server-side validation — the server
|
|
154
|
+
* responds with 400 and code `metadata_invalid`.
|
|
155
|
+
*
|
|
156
|
+
* The `detail` field carries the specific reason from the server — e.g.
|
|
157
|
+
* `'key "foo bar": must match [a-z0-9_]+'`, `'exceeds 50 keys (got 53)'`,
|
|
158
|
+
* or `'exceeds 8192 bytes total (sum of all key + value lengths)'`. The
|
|
159
|
+
* detail names the failing key and rule so callers can fix the offending
|
|
160
|
+
* entry without guessing what shape rule was violated.
|
|
161
|
+
*
|
|
162
|
+
* Validation rules (enforced server-side):
|
|
163
|
+
* - At most 50 keys per object.
|
|
164
|
+
* - At most 8KB total (sum of byte lengths across all keys + values).
|
|
165
|
+
* - Keys: 1–64 chars, must match `[a-z0-9_]+`.
|
|
166
|
+
* - Keys with a leading underscore are reserved for system use.
|
|
167
|
+
* - Values: at most 1024 chars each.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* try {
|
|
171
|
+
* await Koolbase.storage.updateMetadata('photos', 'sunset.jpg', {
|
|
172
|
+
* tag: 'sunset',
|
|
173
|
+
* 'BAD KEY': 'oops',
|
|
174
|
+
* });
|
|
175
|
+
* } catch (e) {
|
|
176
|
+
* if (e instanceof KoolbaseStorageMetadataInvalidError) {
|
|
177
|
+
* console.warn('Metadata rejected:', e.detail);
|
|
178
|
+
* // -> 'Metadata rejected: key "BAD KEY": must match [a-z0-9_]+'
|
|
179
|
+
* }
|
|
180
|
+
* }
|
|
181
|
+
*/
|
|
182
|
+
class KoolbaseStorageMetadataInvalidError extends KoolbaseStorageError {
|
|
183
|
+
constructor(message, detail) {
|
|
184
|
+
super(message ?? 'Metadata payload is invalid', 'metadata_invalid');
|
|
185
|
+
this.detail = detail;
|
|
186
|
+
this.name = 'KoolbaseStorageMetadataInvalidError';
|
|
187
|
+
Object.setPrototypeOf(this, KoolbaseStorageMetadataInvalidError.prototype);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.KoolbaseStorageMetadataInvalidError = KoolbaseStorageMetadataInvalidError;
|
|
151
191
|
/**
|
|
152
192
|
* Maps a non-2xx storage-layer response to a typed
|
|
153
193
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
@@ -173,6 +213,8 @@ function koolbaseStorageError(status, body, fallbackMessage = 'Storage request f
|
|
|
173
213
|
return new KoolbaseStorageFileTooLargeError(message);
|
|
174
214
|
case 'mime_not_allowed':
|
|
175
215
|
return new KoolbaseStorageMimeTypeError(message);
|
|
216
|
+
case 'metadata_invalid':
|
|
217
|
+
return new KoolbaseStorageMetadataInvalidError(message, body?.detail);
|
|
176
218
|
}
|
|
177
219
|
// ─── status fallback (pre-code servers or uncoded paths) ───
|
|
178
220
|
switch (status) {
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KoolbaseConfig, UploadOptions, UploadResult } from './types';
|
|
1
|
+
import { KoolbaseConfig, UploadOptions, UploadResult, KoolbaseObject } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Koolbase storage client — uploads, downloads, and deletes via presigned
|
|
4
4
|
* Cloudflare R2 URLs.
|
|
@@ -23,15 +23,90 @@ export declare class KoolbaseStorage {
|
|
|
23
23
|
* Set `overwrite: true` for true upsert semantics — silently replace any
|
|
24
24
|
* existing object at this path.
|
|
25
25
|
*
|
|
26
|
+
* Pass `options.metadata` to attach arbitrary user-defined key/value pairs
|
|
27
|
+
* to the object at confirm time. Subject to the limits documented on
|
|
28
|
+
* {@link KoolbaseObject.metadata}; violations throw
|
|
29
|
+
* `KoolbaseStorageMetadataInvalidError`. On the `overwrite: true` path the
|
|
30
|
+
* metadata REPLACES any prior metadata at this path (matches GCS semantics).
|
|
31
|
+
* Use {@link updateMetadata} for post-upload merge changes.
|
|
32
|
+
*
|
|
26
33
|
* **Breaking change in v5.0.0**: the default flipped from silent overwrite
|
|
27
34
|
* (legacy behavior) to safe-by-default. If you previously relied on uploads
|
|
28
35
|
* overwriting silently, pass `overwrite: true` explicitly.
|
|
29
36
|
*/
|
|
30
37
|
upload(options: UploadOptions): Promise<UploadResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Apply a partial metadata update to an existing object. Returns the
|
|
40
|
+
* post-update {@link KoolbaseObject} with the merged metadata.
|
|
41
|
+
*
|
|
42
|
+
* **Merge semantics** (mirrors the server's JSONB merge):
|
|
43
|
+
*
|
|
44
|
+
* - Keys with a non-null string value are SET — added if missing,
|
|
45
|
+
* replacing any existing value at the key otherwise.
|
|
46
|
+
* - Keys with `null` are DELETED from the stored metadata.
|
|
47
|
+
* - Keys ABSENT from `metadata` are untouched — pre-existing entries
|
|
48
|
+
* for those keys remain unchanged.
|
|
49
|
+
*
|
|
50
|
+
* Validation runs server-side against the same rules as upload-time
|
|
51
|
+
* metadata; violations throw `KoolbaseStorageMetadataInvalidError`,
|
|
52
|
+
* whose `detail` field names the failing key and rule. The check is
|
|
53
|
+
* performed against the projected post-merge state, so adding a key
|
|
54
|
+
* that would push the object past the 50-key or 8KB ceiling is
|
|
55
|
+
* rejected before the row is mutated.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Add a tag, update an existing key, and drop another in one call:
|
|
59
|
+
* const updated = await Koolbase.storage.updateMetadata(
|
|
60
|
+
* 'photos',
|
|
61
|
+
* 'sunset.jpg',
|
|
62
|
+
* {
|
|
63
|
+
* category: 'landscape', // SET or UPDATE
|
|
64
|
+
* tag: 'sunset', // SET or UPDATE
|
|
65
|
+
* owner: null, // DELETE
|
|
66
|
+
* }
|
|
67
|
+
* );
|
|
68
|
+
* console.log(updated.metadata);
|
|
69
|
+
* // -> { category: 'landscape', tag: 'sunset' }
|
|
70
|
+
*/
|
|
71
|
+
updateMetadata(bucket: string, path: string, metadata: Record<string, string | null>): Promise<KoolbaseObject>;
|
|
31
72
|
/**
|
|
32
73
|
* Get a signed download URL for a file.
|
|
33
74
|
*/
|
|
34
75
|
getDownloadUrl(bucket: string, path: string): Promise<string>;
|
|
76
|
+
/**
|
|
77
|
+
* Build the stable public CDN URL for a file in a public bucket.
|
|
78
|
+
*
|
|
79
|
+
* Returns the URL unconditionally — no check on whether the file
|
|
80
|
+
* exists or whether the bucket is actually public. Use when you
|
|
81
|
+
* know the file is in a public bucket and want the URL without a
|
|
82
|
+
* network round-trip (build-time URL generation, server-side
|
|
83
|
+
* rendering, batch image processing, etc.).
|
|
84
|
+
*
|
|
85
|
+
* For safer construction from an Object you already have, use
|
|
86
|
+
* {@link KoolbaseStorage.publicUrlForObject} — it checks the stored
|
|
87
|
+
* `r2Bucket` value and returns `null` when the object isn't in the
|
|
88
|
+
* public R2 bucket.
|
|
89
|
+
*/
|
|
90
|
+
static publicUrl(args: {
|
|
91
|
+
projectId: string;
|
|
92
|
+
bucket: string;
|
|
93
|
+
path: string;
|
|
94
|
+
}): string;
|
|
95
|
+
/**
|
|
96
|
+
* Returns the stable CDN URL for an object when its bytes physically
|
|
97
|
+
* live in the public R2 bucket, `null` otherwise.
|
|
98
|
+
*
|
|
99
|
+
* Returns `null` for:
|
|
100
|
+
* - Files in private buckets (no public URL ever)
|
|
101
|
+
* - Legacy files in public buckets whose bytes still live in the
|
|
102
|
+
* private R2 bucket from before Gap #2 (no permanent URL until
|
|
103
|
+
* they're re-uploaded)
|
|
104
|
+
*
|
|
105
|
+
* The bucket name must be supplied because {@link KoolbaseObject}
|
|
106
|
+
* carries only the bucket ID, not its name. Typically the caller
|
|
107
|
+
* already knows which bucket they queried.
|
|
108
|
+
*/
|
|
109
|
+
static publicUrlForObject(obj: KoolbaseObject, bucket: string): string | null;
|
|
35
110
|
/**
|
|
36
111
|
* Delete a file from a bucket.
|
|
37
112
|
*/
|
package/dist/storage.js
CHANGED
|
@@ -33,6 +33,13 @@ class KoolbaseStorage {
|
|
|
33
33
|
* Set `overwrite: true` for true upsert semantics — silently replace any
|
|
34
34
|
* existing object at this path.
|
|
35
35
|
*
|
|
36
|
+
* Pass `options.metadata` to attach arbitrary user-defined key/value pairs
|
|
37
|
+
* to the object at confirm time. Subject to the limits documented on
|
|
38
|
+
* {@link KoolbaseObject.metadata}; violations throw
|
|
39
|
+
* `KoolbaseStorageMetadataInvalidError`. On the `overwrite: true` path the
|
|
40
|
+
* metadata REPLACES any prior metadata at this path (matches GCS semantics).
|
|
41
|
+
* Use {@link updateMetadata} for post-upload merge changes.
|
|
42
|
+
*
|
|
36
43
|
* **Breaking change in v5.0.0**: the default flipped from silent overwrite
|
|
37
44
|
* (legacy behavior) to safe-by-default. If you previously relied on uploads
|
|
38
45
|
* overwriting silently, pass `overwrite: true` explicitly.
|
|
@@ -76,20 +83,28 @@ class KoolbaseStorage {
|
|
|
76
83
|
}
|
|
77
84
|
const etag = uploadRes.headers.get('etag') ?? '';
|
|
78
85
|
// ─── Step 3: Confirm upload ───
|
|
86
|
+
// Build the body conditionally so the `metadata` field is only sent
|
|
87
|
+
// when the caller passed it — keeps the wire shape clean for callers
|
|
88
|
+
// that don't care, and lets the server's omitempty path treat absent
|
|
89
|
+
// as "no metadata."
|
|
90
|
+
const confirmBody = {
|
|
91
|
+
bucket: options.bucket,
|
|
92
|
+
path: options.path,
|
|
93
|
+
size: fileSize,
|
|
94
|
+
content_type: contentType,
|
|
95
|
+
etag,
|
|
96
|
+
overwrite,
|
|
97
|
+
};
|
|
98
|
+
if (options.metadata !== undefined) {
|
|
99
|
+
confirmBody.metadata = options.metadata;
|
|
100
|
+
}
|
|
79
101
|
const confirmRes = await fetch(`${this.config.baseUrl}/v1/sdk/storage/confirm`, {
|
|
80
102
|
method: 'POST',
|
|
81
103
|
headers: {
|
|
82
104
|
...(await this.buildHeaders()),
|
|
83
105
|
'Content-Type': 'application/json',
|
|
84
106
|
},
|
|
85
|
-
body: JSON.stringify(
|
|
86
|
-
bucket: options.bucket,
|
|
87
|
-
path: options.path,
|
|
88
|
-
size: fileSize,
|
|
89
|
-
content_type: contentType,
|
|
90
|
-
etag,
|
|
91
|
-
overwrite,
|
|
92
|
-
}),
|
|
107
|
+
body: JSON.stringify(confirmBody),
|
|
93
108
|
});
|
|
94
109
|
if (!confirmRes.ok) {
|
|
95
110
|
throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(confirmRes, 'Failed to confirm upload');
|
|
@@ -100,6 +115,54 @@ class KoolbaseStorage {
|
|
|
100
115
|
const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
|
|
101
116
|
return { object, downloadUrl };
|
|
102
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply a partial metadata update to an existing object. Returns the
|
|
120
|
+
* post-update {@link KoolbaseObject} with the merged metadata.
|
|
121
|
+
*
|
|
122
|
+
* **Merge semantics** (mirrors the server's JSONB merge):
|
|
123
|
+
*
|
|
124
|
+
* - Keys with a non-null string value are SET — added if missing,
|
|
125
|
+
* replacing any existing value at the key otherwise.
|
|
126
|
+
* - Keys with `null` are DELETED from the stored metadata.
|
|
127
|
+
* - Keys ABSENT from `metadata` are untouched — pre-existing entries
|
|
128
|
+
* for those keys remain unchanged.
|
|
129
|
+
*
|
|
130
|
+
* Validation runs server-side against the same rules as upload-time
|
|
131
|
+
* metadata; violations throw `KoolbaseStorageMetadataInvalidError`,
|
|
132
|
+
* whose `detail` field names the failing key and rule. The check is
|
|
133
|
+
* performed against the projected post-merge state, so adding a key
|
|
134
|
+
* that would push the object past the 50-key or 8KB ceiling is
|
|
135
|
+
* rejected before the row is mutated.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* // Add a tag, update an existing key, and drop another in one call:
|
|
139
|
+
* const updated = await Koolbase.storage.updateMetadata(
|
|
140
|
+
* 'photos',
|
|
141
|
+
* 'sunset.jpg',
|
|
142
|
+
* {
|
|
143
|
+
* category: 'landscape', // SET or UPDATE
|
|
144
|
+
* tag: 'sunset', // SET or UPDATE
|
|
145
|
+
* owner: null, // DELETE
|
|
146
|
+
* }
|
|
147
|
+
* );
|
|
148
|
+
* console.log(updated.metadata);
|
|
149
|
+
* // -> { category: 'landscape', tag: 'sunset' }
|
|
150
|
+
*/
|
|
151
|
+
async updateMetadata(bucket, path, metadata) {
|
|
152
|
+
const res = await fetch(`${this.config.baseUrl}/v1/sdk/storage/objects/metadata`, {
|
|
153
|
+
method: 'PATCH',
|
|
154
|
+
headers: {
|
|
155
|
+
...(await this.buildHeaders()),
|
|
156
|
+
'Content-Type': 'application/json',
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({ bucket, path, metadata }),
|
|
159
|
+
});
|
|
160
|
+
if (!res.ok) {
|
|
161
|
+
throw await (0, storage_errors_1.koolbaseStorageErrorFromResponse)(res, 'Failed to update metadata');
|
|
162
|
+
}
|
|
163
|
+
const raw = await res.json();
|
|
164
|
+
return mapObjectFromServer(raw);
|
|
165
|
+
}
|
|
103
166
|
/**
|
|
104
167
|
* Get a signed download URL for a file.
|
|
105
168
|
*/
|
|
@@ -113,6 +176,45 @@ class KoolbaseStorage {
|
|
|
113
176
|
const data = (await res.json());
|
|
114
177
|
return data.url;
|
|
115
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Build the stable public CDN URL for a file in a public bucket.
|
|
181
|
+
*
|
|
182
|
+
* Returns the URL unconditionally — no check on whether the file
|
|
183
|
+
* exists or whether the bucket is actually public. Use when you
|
|
184
|
+
* know the file is in a public bucket and want the URL without a
|
|
185
|
+
* network round-trip (build-time URL generation, server-side
|
|
186
|
+
* rendering, batch image processing, etc.).
|
|
187
|
+
*
|
|
188
|
+
* For safer construction from an Object you already have, use
|
|
189
|
+
* {@link KoolbaseStorage.publicUrlForObject} — it checks the stored
|
|
190
|
+
* `r2Bucket` value and returns `null` when the object isn't in the
|
|
191
|
+
* public R2 bucket.
|
|
192
|
+
*/
|
|
193
|
+
static publicUrl(args) {
|
|
194
|
+
// Encode each path segment individually so slashes are preserved
|
|
195
|
+
// while spaces, parens, hashes, and query characters are escaped.
|
|
196
|
+
const encoded = args.path.split('/').map(encodeURIComponent).join('/');
|
|
197
|
+
return `https://cdn.koolbase.com/${args.projectId}/${args.bucket}/${encoded}`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the stable CDN URL for an object when its bytes physically
|
|
201
|
+
* live in the public R2 bucket, `null` otherwise.
|
|
202
|
+
*
|
|
203
|
+
* Returns `null` for:
|
|
204
|
+
* - Files in private buckets (no public URL ever)
|
|
205
|
+
* - Legacy files in public buckets whose bytes still live in the
|
|
206
|
+
* private R2 bucket from before Gap #2 (no permanent URL until
|
|
207
|
+
* they're re-uploaded)
|
|
208
|
+
*
|
|
209
|
+
* The bucket name must be supplied because {@link KoolbaseObject}
|
|
210
|
+
* carries only the bucket ID, not its name. Typically the caller
|
|
211
|
+
* already knows which bucket they queried.
|
|
212
|
+
*/
|
|
213
|
+
static publicUrlForObject(obj, bucket) {
|
|
214
|
+
if (obj.r2Bucket !== 'koolbase-storage-public')
|
|
215
|
+
return null;
|
|
216
|
+
return KoolbaseStorage.publicUrl({ projectId: obj.projectId, bucket, path: obj.path });
|
|
217
|
+
}
|
|
116
218
|
/**
|
|
117
219
|
* Delete a file from a bucket.
|
|
118
220
|
*/
|
|
@@ -135,6 +237,9 @@ class KoolbaseStorage {
|
|
|
135
237
|
exports.KoolbaseStorage = KoolbaseStorage;
|
|
136
238
|
/**
|
|
137
239
|
* Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
|
|
240
|
+
* Defensive: missing or null `metadata` (older / non-Koolbase responses)
|
|
241
|
+
* is coerced to an empty object so callers always see a typed
|
|
242
|
+
* `Record<string, string>` rather than null.
|
|
138
243
|
*/
|
|
139
244
|
function mapObjectFromServer(raw) {
|
|
140
245
|
return {
|
|
@@ -145,6 +250,8 @@ function mapObjectFromServer(raw) {
|
|
|
145
250
|
path: raw.path,
|
|
146
251
|
size: raw.size ?? 0,
|
|
147
252
|
contentType: raw.content_type ?? null,
|
|
253
|
+
metadata: raw.metadata ?? {},
|
|
254
|
+
r2Bucket: raw.r2_bucket ?? 'koolbase-storage',
|
|
148
255
|
createdAt: raw.created_at,
|
|
149
256
|
updatedAt: raw.updated_at,
|
|
150
257
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -161,6 +161,20 @@ export interface UploadOptions {
|
|
|
161
161
|
* `true` to silently replace the existing object.
|
|
162
162
|
*/
|
|
163
163
|
overwrite?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* User-defined key/value metadata to attach to the object at confirm
|
|
166
|
+
* time. Optional — when omitted, the object stores empty metadata `{}`.
|
|
167
|
+
*
|
|
168
|
+
* Subject to server-side validation (≤50 keys, ≤8KB total, keys 1–64
|
|
169
|
+
* chars matching `[a-z0-9_]+`, values ≤1024 chars, leading underscore
|
|
170
|
+
* reserved); violations throw `KoolbaseStorageMetadataInvalidError`.
|
|
171
|
+
*
|
|
172
|
+
* On the `overwrite: true` path, metadata REPLACES any prior metadata
|
|
173
|
+
* at this path (matches GCS semantics — a new upload at a path
|
|
174
|
+
* produces a new object, not a patch of the old). Use `updateMetadata`
|
|
175
|
+
* for post-upload merge changes.
|
|
176
|
+
*/
|
|
177
|
+
metadata?: Record<string, string>;
|
|
164
178
|
onProgress?: (percent: number) => void;
|
|
165
179
|
}
|
|
166
180
|
/**
|
|
@@ -171,10 +185,28 @@ export interface KoolbaseObject {
|
|
|
171
185
|
id: string;
|
|
172
186
|
projectId: string;
|
|
173
187
|
bucketId: string;
|
|
188
|
+
/**
|
|
189
|
+
* Name of the physical R2 bucket holding this object's bytes
|
|
190
|
+
* (Gap #2). `'koolbase-storage-public'` means the object has a
|
|
191
|
+
* stable CDN URL — construct it with
|
|
192
|
+
* `KoolbaseStorage.publicUrlForObject(obj, bucket)`. Anything else
|
|
193
|
+
* (typically `'koolbase-storage'`) means the object is in private
|
|
194
|
+
* storage and reads go through {@link KoolbaseStorage.getDownloadUrl},
|
|
195
|
+
* which returns a 1-hour presigned URL.
|
|
196
|
+
*/
|
|
197
|
+
r2Bucket: string;
|
|
174
198
|
userId: string | null;
|
|
175
199
|
path: string;
|
|
176
200
|
size: number;
|
|
177
201
|
contentType: string | null;
|
|
202
|
+
/**
|
|
203
|
+
* User-defined key/value metadata attached to this object. Always
|
|
204
|
+
* non-null — empty object when no metadata has been set (the server
|
|
205
|
+
* returns `{}` rather than `null` so callers can treat it as a
|
|
206
|
+
* guaranteed object without null checks). Set on upload via
|
|
207
|
+
* `upload({ metadata })` or mutated post-upload via `updateMetadata`.
|
|
208
|
+
*/
|
|
209
|
+
metadata: Record<string, string>;
|
|
178
210
|
/** ISO 8601 timestamp from the server. */
|
|
179
211
|
createdAt: string;
|
|
180
212
|
/** ISO 8601 timestamp from the server. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techfinityedge/koolbase-react-native",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/jszip": "^3.4.0",
|
|
30
|
+
"@types/node": "^25.9.1",
|
|
31
|
+
"react-native": "^0.85.3",
|
|
30
32
|
"react-native-keychain": "^10.0.0",
|
|
31
33
|
"typescript": "^5.0.0"
|
|
32
34
|
},
|