@techfinityedge/koolbase-react-native 2.1.0 → 2.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 CHANGED
@@ -162,6 +162,20 @@ await Koolbase.db.update('record-id', { title: 'Updated' });
162
162
  await Koolbase.db.delete('record-id');
163
163
  ```
164
164
 
165
+ ### Handling unique-constraint conflicts
166
+
167
+ A write that would violate a unique constraint throws `KoolbaseConflictError`:
168
+
169
+ \`\`\`ts
170
+ try {
171
+ await koolbase.db.upsert('users', { email }, { name });
172
+ } catch (e) {
173
+ if (e instanceof KoolbaseConflictError) {
174
+ showError('That email is already registered.');
175
+ }
176
+ }
177
+ \`\`\`
178
+
165
179
  ### Upsert
166
180
 
167
181
  Insert a record, or update the existing one matching a filter.
@@ -388,6 +402,44 @@ await Koolbase.messaging.send({
388
402
 
389
403
  ---
390
404
 
405
+ ## Error handling
406
+
407
+ Koolbase throws typed errors selected from the server's stable error `code`, so
408
+ handling doesn't depend on message text.
409
+
410
+ ### Database errors
411
+
412
+ All data-layer failures extend `KoolbaseDataError` (which extends `Error`):
413
+
414
+ | Error | When |
415
+ |---|---|
416
+ | `KoolbaseConflictError` | A write violates a unique constraint (409). Exposes `.field`. |
417
+ | `KoolbaseNotFoundError` | The record or collection doesn't exist (404). |
418
+ | `KoolbaseValidationError` | The request was rejected as invalid (400). |
419
+ | `KoolbasePermissionError` | An access rule denied the operation (403). |
420
+ | `KoolbaseRateLimitError` | The caller is being rate-limited (429). |
421
+
422
+ ```ts
423
+ import { KoolbaseConflictError, KoolbaseDataError } from '@techfinityedge/koolbase-react-native';
424
+
425
+ try {
426
+ await koolbase.db.upsert('users', { email }, { name });
427
+ } catch (e) {
428
+ if (e instanceof KoolbaseConflictError) {
429
+ showError(`That ${e.field ?? 'value'} is already taken.`);
430
+ } else if (e instanceof KoolbaseDataError) {
431
+ showError(e.message);
432
+ }
433
+ }
434
+ ```
435
+
436
+ > `query`, `get`, `upsert`, and `deleteWhere` throw these typed errors. `insert`,
437
+ > `update`, and `delete` are optimistic/offline-first — they queue and sync in
438
+ > the background, so their conflicts surface via the sync engine, not as a
439
+ > thrown error.
440
+
441
+ ---
442
+
391
443
  ## What's included
392
444
 
393
445
  - Authentication: email + password, Apple Sign-In, phone + OTP
package/dist/auth.d.ts CHANGED
@@ -110,15 +110,18 @@ export declare class KoolbaseAuth {
110
110
  */
111
111
  signInWithGoogle(params: SignInWithGoogleParams): Promise<KoolbaseSession>;
112
112
  /**
113
- * Parses a /v1/sdk/auth/oauth/google response. Distinct from
114
- * parseSessionResponse and parseAppleSessionResponse because OAuth
115
- * error semantics differ per-provider.
113
+ * Parses a /v1/sdk/auth/oauth/google response. Code-first: the server
114
+ * emits unified OAuth codes (oauth_not_configured, invalid_oauth_token,
115
+ * oauth_email_required, oauth_email_conflict) for both providers; the
116
+ * provider distinction is made here so Google codes map to Google-specific
117
+ * errors. Status + message logic is retained as a fallback for older servers.
116
118
  */
117
119
  private parseGoogleSessionResponse;
118
120
  /**
119
- * Parses a /v1/sdk/auth/oauth/apple response. Distinct from
120
- * parseSessionResponse because OAuth error semantics differ from
121
- * credential auth status codes map to a separate error set.
121
+ * Parses a /v1/sdk/auth/oauth/apple response. Code-first; the provider
122
+ * distinction is made here so the server's unified OAuth codes map to
123
+ * Apple-specific errors. Status + message logic is retained as a fallback
124
+ * for older servers.
122
125
  */
123
126
  private parseAppleSessionResponse;
124
127
  refresh(refreshToken?: string): Promise<KoolbaseSession>;
@@ -159,8 +162,30 @@ export declare class KoolbaseAuth {
159
162
  private validatePhone;
160
163
  private _ensureValidToken;
161
164
  private mapUser;
165
+ /**
166
+ * Parse a session-returning response (login, register, refresh).
167
+ * Non-2xx is delegated to throwTypedError, which is code-first
168
+ * (reads body.code) with a status/message fallback. isRefresh only
169
+ * affects how a bare 401 (no code, older server) is interpreted.
170
+ */
162
171
  private parseSessionResponse;
163
172
  private checkResponse;
173
+ /**
174
+ * Map a non-2xx credential/session response to a typed error.
175
+ *
176
+ * Code-first: the server now emits a stable `code` on every error
177
+ * (contract conformance), so we switch on body.code. The status +
178
+ * message logic is retained as a fallback for older servers or any
179
+ * response that arrives without a code. isRefresh only changes how a
180
+ * bare 401 is interpreted in the fallback path.
181
+ */
164
182
  private throwTypedError;
183
+ /**
184
+ * Parse a phone-auth response. Code-first, with a phone-specific twist:
185
+ * the server emits the generic `rate_limit` code for the phone endpoints
186
+ * (they share the default 429), but phone has a dedicated server-side
187
+ * rate-limiter, so we surface OtpRateLimitError rather than RateLimitError.
188
+ * Status + message logic is retained as a fallback for older servers.
189
+ */
165
190
  private parsePhoneResponse;
166
191
  }
package/dist/auth.js CHANGED
@@ -297,9 +297,11 @@ class KoolbaseAuth {
297
297
  return session;
298
298
  }
299
299
  /**
300
- * Parses a /v1/sdk/auth/oauth/google response. Distinct from
301
- * parseSessionResponse and parseAppleSessionResponse because OAuth
302
- * error semantics differ per-provider.
300
+ * Parses a /v1/sdk/auth/oauth/google response. Code-first: the server
301
+ * emits unified OAuth codes (oauth_not_configured, invalid_oauth_token,
302
+ * oauth_email_required, oauth_email_conflict) for both providers; the
303
+ * provider distinction is made here so Google codes map to Google-specific
304
+ * errors. Status + message logic is retained as a fallback for older servers.
303
305
  */
304
306
  async parseGoogleSessionResponse(res) {
305
307
  if (res.status === 200) {
@@ -311,14 +313,31 @@ class KoolbaseAuth {
311
313
  user: this.mapUser(data.user),
312
314
  };
313
315
  }
314
- let errorMessage = '';
316
+ let body = {};
315
317
  try {
316
- const data = await res.json();
317
- errorMessage = data?.error ?? '';
318
+ body = await res.json();
318
319
  }
319
320
  catch {
320
321
  // best-effort error message extraction
321
322
  }
323
+ const code = body?.code ?? '';
324
+ const errorMessage = body?.error ?? '';
325
+ // ─── code-first ───
326
+ switch (code) {
327
+ case 'oauth_not_configured':
328
+ throw new auth_errors_1.GoogleSignInNotConfiguredError();
329
+ case 'invalid_oauth_token':
330
+ throw new auth_errors_1.InvalidGoogleTokenError();
331
+ case 'account_disabled':
332
+ throw new auth_errors_1.UserDisabledError();
333
+ case 'oauth_email_required':
334
+ throw new auth_errors_1.GoogleEmailRequiredError();
335
+ case 'oauth_email_conflict':
336
+ throw new auth_errors_1.OAuthEmailConflictError();
337
+ case 'rate_limit':
338
+ throw new auth_errors_1.RateLimitError(errorMessage || undefined);
339
+ }
340
+ // ─── status + message fallback (pre-code servers) ───
322
341
  if (res.status === 400) {
323
342
  if (errorMessage.includes('not configured')) {
324
343
  throw new auth_errors_1.GoogleSignInNotConfiguredError();
@@ -339,9 +358,10 @@ class KoolbaseAuth {
339
358
  throw new auth_errors_1.KoolbaseAuthError(`google sign-in failed: ${res.status} ${errorMessage}`, `google_signin_http_${res.status}`);
340
359
  }
341
360
  /**
342
- * Parses a /v1/sdk/auth/oauth/apple response. Distinct from
343
- * parseSessionResponse because OAuth error semantics differ from
344
- * credential auth status codes map to a separate error set.
361
+ * Parses a /v1/sdk/auth/oauth/apple response. Code-first; the provider
362
+ * distinction is made here so the server's unified OAuth codes map to
363
+ * Apple-specific errors. Status + message logic is retained as a fallback
364
+ * for older servers.
345
365
  */
346
366
  async parseAppleSessionResponse(res) {
347
367
  if (res.status === 200) {
@@ -353,14 +373,31 @@ class KoolbaseAuth {
353
373
  user: this.mapUser(data.user),
354
374
  };
355
375
  }
356
- let errorMessage = '';
376
+ let body = {};
357
377
  try {
358
- const data = await res.json();
359
- errorMessage = data?.error ?? '';
378
+ body = await res.json();
360
379
  }
361
380
  catch {
362
381
  // best-effort error message extraction
363
382
  }
383
+ const code = body?.code ?? '';
384
+ const errorMessage = body?.error ?? '';
385
+ // ─── code-first ───
386
+ switch (code) {
387
+ case 'oauth_not_configured':
388
+ throw new auth_errors_1.AppleSignInNotConfiguredError();
389
+ case 'invalid_oauth_token':
390
+ throw new auth_errors_1.InvalidAppleTokenError();
391
+ case 'account_disabled':
392
+ throw new auth_errors_1.UserDisabledError();
393
+ case 'oauth_email_required':
394
+ throw new auth_errors_1.AppleEmailRequiredError();
395
+ case 'oauth_email_conflict':
396
+ throw new auth_errors_1.OAuthEmailConflictError();
397
+ case 'rate_limit':
398
+ throw new auth_errors_1.RateLimitError(errorMessage || undefined);
399
+ }
400
+ // ─── status + message fallback (pre-code servers) ───
364
401
  if (res.status === 400) {
365
402
  if (errorMessage.includes('not configured')) {
366
403
  throw new auth_errors_1.AppleSignInNotConfiguredError();
@@ -592,16 +629,15 @@ class KoolbaseAuth {
592
629
  createdAt: raw.created_at,
593
630
  };
594
631
  }
632
+ /**
633
+ * Parse a session-returning response (login, register, refresh).
634
+ * Non-2xx is delegated to throwTypedError, which is code-first
635
+ * (reads body.code) with a status/message fallback. isRefresh only
636
+ * affects how a bare 401 (no code, older server) is interpreted.
637
+ */
595
638
  async parseSessionResponse(res, isRefresh) {
596
- if (res.status === 409)
597
- throw new auth_errors_1.EmailAlreadyInUseError();
598
- if (res.status === 401) {
599
- throw isRefresh ? new auth_errors_1.SessionExpiredError() : new auth_errors_1.InvalidCredentialsError();
600
- }
601
- if (res.status === 403)
602
- throw new auth_errors_1.UserDisabledError();
603
639
  if (!res.ok)
604
- await this.throwTypedError(res);
640
+ await this.throwTypedError(res, isRefresh); // never returns
605
641
  const data = await res.json();
606
642
  return {
607
643
  accessToken: data.access_token,
@@ -615,7 +651,16 @@ class KoolbaseAuth {
615
651
  return;
616
652
  await this.throwTypedError(res);
617
653
  }
618
- async throwTypedError(res) {
654
+ /**
655
+ * Map a non-2xx credential/session response to a typed error.
656
+ *
657
+ * Code-first: the server now emits a stable `code` on every error
658
+ * (contract conformance), so we switch on body.code. The status +
659
+ * message logic is retained as a fallback for older servers or any
660
+ * response that arrives without a code. isRefresh only changes how a
661
+ * bare 401 is interpreted in the fallback path.
662
+ */
663
+ async throwTypedError(res, isRefresh = false) {
619
664
  let body = {};
620
665
  try {
621
666
  body = await res.json();
@@ -623,13 +668,43 @@ class KoolbaseAuth {
623
668
  catch {
624
669
  // ignore
625
670
  }
671
+ const code = body.code ?? '';
626
672
  const msg = body.error ?? '';
673
+ // ─── code-first ───
674
+ switch (code) {
675
+ case 'invalid_credentials':
676
+ throw new auth_errors_1.InvalidCredentialsError();
677
+ case 'email_in_use':
678
+ throw new auth_errors_1.EmailAlreadyInUseError();
679
+ case 'account_disabled':
680
+ throw new auth_errors_1.UserDisabledError();
681
+ case 'account_locked':
682
+ throw new auth_errors_1.AccountLockedError();
683
+ case 'invalid_refresh_token':
684
+ // Refresh token rejected — the session is unrecoverable; re-login.
685
+ throw new auth_errors_1.SessionExpiredError();
686
+ case 'token_revoked':
687
+ throw new auth_errors_1.TokenRevokedError();
688
+ case 'invalid_unlock_token':
689
+ throw new auth_errors_1.UnlockTokenInvalidError();
690
+ case 'rate_limit':
691
+ throw new auth_errors_1.RateLimitError(msg || undefined);
692
+ }
693
+ // ─── status fallback (pre-code servers) ───
694
+ if (res.status === 409)
695
+ throw new auth_errors_1.EmailAlreadyInUseError();
696
+ if (res.status === 401) {
697
+ throw isRefresh ? new auth_errors_1.SessionExpiredError() : new auth_errors_1.InvalidCredentialsError();
698
+ }
699
+ if (res.status === 403)
700
+ throw new auth_errors_1.UserDisabledError();
627
701
  if (res.status === 429) {
628
702
  if (msg.includes('account temporarily locked')) {
629
703
  throw new auth_errors_1.AccountLockedError();
630
704
  }
631
705
  throw new auth_errors_1.RateLimitError(msg || undefined);
632
706
  }
707
+ // ─── legacy message fallback ───
633
708
  if (msg.includes('invalid or expired unlock token')) {
634
709
  throw new auth_errors_1.UnlockTokenInvalidError();
635
710
  }
@@ -638,8 +713,15 @@ class KoolbaseAuth {
638
713
  msg.includes('session has been revoked')) {
639
714
  throw new auth_errors_1.TokenRevokedError();
640
715
  }
641
- throw new auth_errors_1.KoolbaseAuthError(msg || `Request failed: ${res.status}`, `http_${res.status}`);
716
+ throw new auth_errors_1.KoolbaseAuthError(msg || `Request failed: ${res.status}`, code || `http_${res.status}`);
642
717
  }
718
+ /**
719
+ * Parse a phone-auth response. Code-first, with a phone-specific twist:
720
+ * the server emits the generic `rate_limit` code for the phone endpoints
721
+ * (they share the default 429), but phone has a dedicated server-side
722
+ * rate-limiter, so we surface OtpRateLimitError rather than RateLimitError.
723
+ * Status + message logic is retained as a fallback for older servers.
724
+ */
643
725
  async parsePhoneResponse(res) {
644
726
  let body = {};
645
727
  try {
@@ -650,11 +732,31 @@ class KoolbaseAuth {
650
732
  }
651
733
  if (res.ok)
652
734
  return body;
735
+ const code = body.code ?? '';
653
736
  const msg = body.error ?? '';
737
+ // ─── code-first ───
738
+ switch (code) {
739
+ case 'invalid_phone':
740
+ throw new auth_errors_1.InvalidPhoneNumberError();
741
+ case 'otp_expired':
742
+ throw new auth_errors_1.OtpExpiredError();
743
+ case 'otp_invalid':
744
+ throw new auth_errors_1.OtpInvalidError();
745
+ case 'otp_max_attempts':
746
+ throw new auth_errors_1.OtpMaxAttemptsError();
747
+ case 'phone_in_use':
748
+ throw new auth_errors_1.PhoneAlreadyLinkedError();
749
+ case 'sms_not_configured':
750
+ throw new auth_errors_1.SmsConfigMissingError();
751
+ case 'rate_limit':
752
+ throw new auth_errors_1.OtpRateLimitError();
753
+ }
754
+ // ─── status fallback (pre-code servers) ───
654
755
  if (res.status === 429)
655
756
  throw new auth_errors_1.OtpRateLimitError();
656
757
  if (res.status === 409)
657
758
  throw new auth_errors_1.PhoneAlreadyLinkedError();
759
+ // ─── legacy message fallback ───
658
760
  if (msg.includes('E.164'))
659
761
  throw new auth_errors_1.InvalidPhoneNumberError();
660
762
  if (msg.includes('OTP has expired'))
@@ -669,7 +771,7 @@ class KoolbaseAuth {
669
771
  if (msg.includes('SMS provider not configured')) {
670
772
  throw new auth_errors_1.SmsConfigMissingError();
671
773
  }
672
- throw new auth_errors_1.KoolbaseAuthError(msg || 'An unexpected error occurred');
774
+ throw new auth_errors_1.KoolbaseAuthError(msg || 'An unexpected error occurred', code || undefined);
673
775
  }
674
776
  }
675
777
  exports.KoolbaseAuth = KoolbaseAuth;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Base class for errors surfaced by the Koolbase data layer (database reads
3
+ * and writes). Every data error carries a `message` and, when the server
4
+ * provides one, its stable `code` (e.g. `not_found`, `validation_error`,
5
+ * `unique_violation`).
6
+ *
7
+ * Catch this to handle any data-layer failure generically, or catch a
8
+ * specific subclass to branch on the kind of failure.
9
+ */
10
+ export declare class KoolbaseDataError extends Error {
11
+ code?: string;
12
+ constructor(message: string, code?: string);
13
+ }
14
+ /**
15
+ * Thrown when a write is rejected because the value would violate a
16
+ * collection's unique constraint — the server responds with 409 Conflict.
17
+ * Catch it to handle duplicates, e.g. an email or username already in use.
18
+ *
19
+ * `field` names the field that collided, when the server reports it
20
+ * (`details.field`) — useful when a collection has more than one unique
21
+ * constraint and you need to know which value clashed.
22
+ *
23
+ * Currently surfaced by `upsert` (the online-only write). `insert` and
24
+ * `update` are optimistic/offline-first: they accept the write locally and
25
+ * sync in the background, so a constraint conflict on those paths is a
26
+ * sync-time concern rather than a thrown error.
27
+ *
28
+ * @example
29
+ * try {
30
+ * await koolbase.db.upsert('users', { email }, { name });
31
+ * } catch (e) {
32
+ * if (e instanceof KoolbaseConflictError) {
33
+ * showError(`That ${e.field ?? 'value'} is already registered.`);
34
+ * }
35
+ * }
36
+ */
37
+ export declare class KoolbaseConflictError extends KoolbaseDataError {
38
+ field?: string;
39
+ constructor(message?: string, field?: string);
40
+ }
41
+ /**
42
+ * Thrown when the requested record or collection does not exist — the server
43
+ * responds with 404 and code `not_found` / `record_not_found` /
44
+ * `collection_not_found`.
45
+ */
46
+ export declare class KoolbaseNotFoundError extends KoolbaseDataError {
47
+ constructor(message?: string);
48
+ }
49
+ /**
50
+ * Thrown when the request is rejected as invalid — the server responds with
51
+ * 400 and code `validation_error`.
52
+ */
53
+ export declare class KoolbaseValidationError extends KoolbaseDataError {
54
+ constructor(message?: string);
55
+ }
56
+ /**
57
+ * Thrown when the caller is authenticated but not allowed to perform the
58
+ * operation — the server responds with 403 and code `permission_denied`
59
+ * (typically a collection access rule rejecting the read/write).
60
+ */
61
+ export declare class KoolbasePermissionError extends KoolbaseDataError {
62
+ constructor(message?: string);
63
+ }
64
+ /**
65
+ * Thrown when the server is rate-limiting the caller — 429 with code
66
+ * `rate_limit`. Back off and retry after a short delay.
67
+ */
68
+ export declare class KoolbaseRateLimitError extends KoolbaseDataError {
69
+ constructor(message?: string);
70
+ }
71
+ /**
72
+ * Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
73
+ * preferring the server's stable `code` and falling back to the HTTP status
74
+ * for older or uncoded responses. Always returns an error to throw.
75
+ */
76
+ export declare function koolbaseDataError(status: number, body: any, fallbackMessage?: string): KoolbaseDataError;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KoolbaseRateLimitError = exports.KoolbasePermissionError = exports.KoolbaseValidationError = exports.KoolbaseNotFoundError = exports.KoolbaseConflictError = exports.KoolbaseDataError = void 0;
4
+ exports.koolbaseDataError = koolbaseDataError;
5
+ /**
6
+ * Base class for errors surfaced by the Koolbase data layer (database reads
7
+ * and writes). Every data error carries a `message` and, when the server
8
+ * provides one, its stable `code` (e.g. `not_found`, `validation_error`,
9
+ * `unique_violation`).
10
+ *
11
+ * Catch this to handle any data-layer failure generically, or catch a
12
+ * specific subclass to branch on the kind of failure.
13
+ */
14
+ class KoolbaseDataError extends Error {
15
+ constructor(message, code) {
16
+ super(message);
17
+ this.code = code;
18
+ this.name = 'KoolbaseDataError';
19
+ Object.setPrototypeOf(this, KoolbaseDataError.prototype);
20
+ }
21
+ }
22
+ exports.KoolbaseDataError = KoolbaseDataError;
23
+ /**
24
+ * Thrown when a write is rejected because the value would violate a
25
+ * collection's unique constraint — the server responds with 409 Conflict.
26
+ * Catch it to handle duplicates, e.g. an email or username already in use.
27
+ *
28
+ * `field` names the field that collided, when the server reports it
29
+ * (`details.field`) — useful when a collection has more than one unique
30
+ * constraint and you need to know which value clashed.
31
+ *
32
+ * Currently surfaced by `upsert` (the online-only write). `insert` and
33
+ * `update` are optimistic/offline-first: they accept the write locally and
34
+ * sync in the background, so a constraint conflict on those paths is a
35
+ * sync-time concern rather than a thrown error.
36
+ *
37
+ * @example
38
+ * try {
39
+ * await koolbase.db.upsert('users', { email }, { name });
40
+ * } catch (e) {
41
+ * if (e instanceof KoolbaseConflictError) {
42
+ * showError(`That ${e.field ?? 'value'} is already registered.`);
43
+ * }
44
+ * }
45
+ */
46
+ class KoolbaseConflictError extends KoolbaseDataError {
47
+ constructor(message, field) {
48
+ super(message ?? 'Value violates a unique constraint', 'unique_violation');
49
+ this.field = field;
50
+ this.name = 'KoolbaseConflictError';
51
+ Object.setPrototypeOf(this, KoolbaseConflictError.prototype);
52
+ }
53
+ }
54
+ exports.KoolbaseConflictError = KoolbaseConflictError;
55
+ /**
56
+ * Thrown when the requested record or collection does not exist — the server
57
+ * responds with 404 and code `not_found` / `record_not_found` /
58
+ * `collection_not_found`.
59
+ */
60
+ class KoolbaseNotFoundError extends KoolbaseDataError {
61
+ constructor(message) {
62
+ super(message ?? 'The requested resource was not found', 'not_found');
63
+ this.name = 'KoolbaseNotFoundError';
64
+ Object.setPrototypeOf(this, KoolbaseNotFoundError.prototype);
65
+ }
66
+ }
67
+ exports.KoolbaseNotFoundError = KoolbaseNotFoundError;
68
+ /**
69
+ * Thrown when the request is rejected as invalid — the server responds with
70
+ * 400 and code `validation_error`.
71
+ */
72
+ class KoolbaseValidationError extends KoolbaseDataError {
73
+ constructor(message) {
74
+ super(message ?? 'The request was invalid', 'validation_error');
75
+ this.name = 'KoolbaseValidationError';
76
+ Object.setPrototypeOf(this, KoolbaseValidationError.prototype);
77
+ }
78
+ }
79
+ exports.KoolbaseValidationError = KoolbaseValidationError;
80
+ /**
81
+ * Thrown when the caller is authenticated but not allowed to perform the
82
+ * operation — the server responds with 403 and code `permission_denied`
83
+ * (typically a collection access rule rejecting the read/write).
84
+ */
85
+ class KoolbasePermissionError extends KoolbaseDataError {
86
+ constructor(message) {
87
+ super(message ?? 'You do not have permission to perform this action', 'permission_denied');
88
+ this.name = 'KoolbasePermissionError';
89
+ Object.setPrototypeOf(this, KoolbasePermissionError.prototype);
90
+ }
91
+ }
92
+ exports.KoolbasePermissionError = KoolbasePermissionError;
93
+ /**
94
+ * Thrown when the server is rate-limiting the caller — 429 with code
95
+ * `rate_limit`. Back off and retry after a short delay.
96
+ */
97
+ class KoolbaseRateLimitError extends KoolbaseDataError {
98
+ constructor(message) {
99
+ super(message ?? 'Too many requests, please slow down', 'rate_limit');
100
+ this.name = 'KoolbaseRateLimitError';
101
+ Object.setPrototypeOf(this, KoolbaseRateLimitError.prototype);
102
+ }
103
+ }
104
+ exports.KoolbaseRateLimitError = KoolbaseRateLimitError;
105
+ /**
106
+ * Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
107
+ * preferring the server's stable `code` and falling back to the HTTP status
108
+ * for older or uncoded responses. Always returns an error to throw.
109
+ */
110
+ function koolbaseDataError(status, body, fallbackMessage = 'Request failed') {
111
+ const code = body?.code;
112
+ const message = body?.error ?? fallbackMessage;
113
+ const field = body?.details?.field;
114
+ // ─── code-first ───
115
+ switch (code) {
116
+ case 'unique_violation':
117
+ return new KoolbaseConflictError(message, field);
118
+ case 'not_found':
119
+ case 'record_not_found':
120
+ case 'collection_not_found':
121
+ return new KoolbaseNotFoundError(message);
122
+ case 'permission_denied':
123
+ return new KoolbasePermissionError(message);
124
+ case 'rate_limit':
125
+ return new KoolbaseRateLimitError(message);
126
+ case 'validation_error':
127
+ return new KoolbaseValidationError(message);
128
+ }
129
+ // ─── status fallback (pre-code servers) ───
130
+ switch (status) {
131
+ case 409:
132
+ return new KoolbaseConflictError(message);
133
+ case 404:
134
+ return new KoolbaseNotFoundError(message);
135
+ case 403:
136
+ return new KoolbasePermissionError(message);
137
+ case 429:
138
+ return new KoolbaseRateLimitError(message);
139
+ case 400:
140
+ return new KoolbaseValidationError(message);
141
+ }
142
+ return new KoolbaseDataError(message, code);
143
+ }
package/dist/database.js CHANGED
@@ -4,6 +4,7 @@ exports.KoolbaseDatabase = void 0;
4
4
  const cache_store_1 = require("./cache-store");
5
5
  const sync_engine_1 = require("./sync-engine");
6
6
  const record_1 = require("./record");
7
+ const database_errors_1 = require("./database-errors");
7
8
  function generateId() {
8
9
  return 'local_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
9
10
  }
@@ -30,7 +31,7 @@ class KoolbaseDatabase {
30
31
  });
31
32
  const data = await res.json();
32
33
  if (!res.ok) {
33
- throw new Error(data.error ?? `Request failed: ${res.status}`);
34
+ throw (0, database_errors_1.koolbaseDataError)(res.status, data, `Request failed: ${res.status}`);
34
35
  }
35
36
  return data;
36
37
  }
@@ -121,7 +122,7 @@ class KoolbaseDatabase {
121
122
  });
122
123
  const body = await res.json();
123
124
  if (!res.ok) {
124
- throw new Error(body.error ?? `Upsert failed: ${res.status}`);
125
+ throw (0, database_errors_1.koolbaseDataError)(res.status, body, `Upsert failed: ${res.status}`);
125
126
  }
126
127
  const created = res.status === 201;
127
128
  const record = (0, record_1.recordFromWire)(body);
@@ -150,7 +151,7 @@ class KoolbaseDatabase {
150
151
  });
151
152
  const body = await res.json();
152
153
  if (!res.ok) {
153
- throw new Error(body.error ?? `Delete failed: ${res.status}`);
154
+ throw (0, database_errors_1.koolbaseDataError)(res.status, body, `Delete failed: ${res.status}`);
154
155
  }
155
156
  const userId = this.getUserId() ?? 'anonymous';
156
157
  await (0, cache_store_1.invalidateCache)(userId, collection);
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ import { KoolbaseStorage } from './storage';
18
18
  import { KoolbaseConfig, VersionCheckResult } from './types';
19
19
  export * from './types';
20
20
  export * from './auth-errors';
21
+ export * from './database-errors';
21
22
  export { KoolbaseAuth, KoolbaseDatabase, KoolbaseFlags, KoolbaseFunctions, KoolbaseRealtime, KoolbaseStorage };
22
23
  export declare const Koolbase: {
23
24
  initialize(config: KoolbaseConfig): Promise<void>;
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ const storage_1 = require("./storage");
45
45
  Object.defineProperty(exports, "KoolbaseStorage", { enumerable: true, get: function () { return storage_1.KoolbaseStorage; } });
46
46
  __exportStar(require("./types"), exports);
47
47
  __exportStar(require("./auth-errors"), exports);
48
+ __exportStar(require("./database-errors"), exports);
48
49
  let _auth = null;
49
50
  let _db = null;
50
51
  let _storage = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "React Native SDK for Koolbase \u2014 auth, database, storage, realtime, feature flags, and functions in one package.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",