@techfinityedge/koolbase-react-native 2.0.0 → 2.2.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,52 @@ 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
+
179
+ ### Upsert
180
+
181
+ Insert a record, or update the existing one matching a filter.
182
+
183
+ \`\`\`ts
184
+ const result = await Koolbase.db.upsert(
185
+ 'profiles',
186
+ { user_id: userId },
187
+ { weightKg: 70 }
188
+ );
189
+
190
+ console.log(result.created); // true if inserted, false if updated
191
+ console.log(result.record.id);
192
+ \`\`\`
193
+
194
+ > Online-only: needs the server's view to decide insert vs update, so unlike
195
+ > `insert` it isn't queued offline and throws on network failure.
196
+
197
+ ### Delete where
198
+
199
+ Bulk-delete every record matching a filter. Returns the number deleted.
200
+
201
+ \`\`\`ts
202
+ const deleted = await Koolbase.db.deleteWhere('sessions', {
203
+ user_id: userId,
204
+ status: 'expired',
205
+ });
206
+ \`\`\`
207
+
208
+ > A non-empty filter is required. The collection's delete rule applies; for
209
+ > `owner`/`scoped` rules the delete is scoped to your own records. Online-only.
210
+
165
211
  ### Offline-first
166
212
 
167
213
  ```typescript
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Thrown when a write is rejected because the value would violate a
3
+ * collection's unique constraint — the server responds with 409 Conflict.
4
+ * Catch it to handle duplicates, e.g. an email or username already in use.
5
+ *
6
+ * Currently surfaced by `upsert` (the online-only write). `insert` and
7
+ * `update` are optimistic/offline-first: they accept the write locally and
8
+ * sync in the background, so a constraint conflict on those paths is a
9
+ * sync-time concern rather than a thrown error.
10
+ *
11
+ * @example
12
+ * try {
13
+ * await koolbase.db.upsert('users', { email }, { name });
14
+ * } catch (e) {
15
+ * if (e instanceof KoolbaseConflictError) {
16
+ * showError('That email is already registered.');
17
+ * }
18
+ * }
19
+ */
20
+ export declare class KoolbaseConflictError extends Error {
21
+ code: string;
22
+ constructor(message?: string);
23
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KoolbaseConflictError = void 0;
4
+ /**
5
+ * Thrown when a write is rejected because the value would violate a
6
+ * collection's unique constraint — the server responds with 409 Conflict.
7
+ * Catch it to handle duplicates, e.g. an email or username already in use.
8
+ *
9
+ * Currently surfaced by `upsert` (the online-only write). `insert` and
10
+ * `update` are optimistic/offline-first: they accept the write locally and
11
+ * sync in the background, so a constraint conflict on those paths is a
12
+ * sync-time concern rather than a thrown error.
13
+ *
14
+ * @example
15
+ * try {
16
+ * await koolbase.db.upsert('users', { email }, { name });
17
+ * } catch (e) {
18
+ * if (e instanceof KoolbaseConflictError) {
19
+ * showError('That email is already registered.');
20
+ * }
21
+ * }
22
+ */
23
+ class KoolbaseConflictError extends Error {
24
+ constructor(message) {
25
+ super(message ?? 'Value violates a unique constraint');
26
+ this.code = 'unique_violation';
27
+ this.name = 'KoolbaseConflictError';
28
+ Object.setPrototypeOf(this, KoolbaseConflictError.prototype);
29
+ }
30
+ }
31
+ exports.KoolbaseConflictError = KoolbaseConflictError;
@@ -1,4 +1,4 @@
1
- import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult } from './types';
1
+ import { KoolbaseConfig, KoolbaseRecord, QueryOptions, QueryResult, UpsertResult } from './types';
2
2
  export declare class KoolbaseDatabase {
3
3
  private config;
4
4
  private getUserId;
@@ -9,6 +9,34 @@ export declare class KoolbaseDatabase {
9
9
  private runQuery;
10
10
  query(collection: string, options?: QueryOptions): Promise<QueryResult>;
11
11
  insert(collection: string, data: Record<string, unknown>): Promise<KoolbaseRecord>;
12
+ /**
13
+ * Insert a record, or update the existing one matching `match`.
14
+ *
15
+ * The server decides: exactly one match updates it, no match inserts a new
16
+ * record (seeded with the `match` fields), more than one match is an error.
17
+ * Returns the resulting record and a `created` flag (true = inserted, false
18
+ * = updated).
19
+ *
20
+ * Online-only by design. Unlike `insert`, an upsert is NOT queued offline:
21
+ * the insert-vs-update decision needs the server's authoritative view of
22
+ * what already exists, so deferring it could create a duplicate or apply a
23
+ * wrong update on later sync. It throws on network failure instead. A raw
24
+ * fetch is used (not `request`) so the status code is readable: 201 =
25
+ * created, 200 = updated.
26
+ */
27
+ upsert(collection: string, match: Record<string, unknown>, data: Record<string, unknown>): Promise<UpsertResult>;
28
+ /**
29
+ * Bulk-delete every record in `collection` matching `filters`.
30
+ *
31
+ * The server applies the collection's delete rule (scoping to the caller for
32
+ * owner/scoped rules) and returns the number of records deleted.
33
+ *
34
+ * Online-only by design — like upsert, this is NOT queued offline: a bulk
35
+ * delete needs the server's authoritative view of what matches, so it throws
36
+ * on network failure rather than risk deleting the wrong set on later sync.
37
+ * The collection cache is invalidated on success.
38
+ */
39
+ deleteWhere(collection: string, filters: Record<string, unknown>): Promise<number>;
12
40
  get(recordId: string): Promise<KoolbaseRecord>;
13
41
  update(recordId: string, data: Record<string, unknown>): Promise<KoolbaseRecord>;
14
42
  delete(recordId: string): Promise<void>;
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
  }
@@ -97,6 +98,68 @@ class KoolbaseDatabase {
97
98
  });
98
99
  return optimisticRecord;
99
100
  }
101
+ // ─── Upsert (online-only) ─────────────────────────────────────────────────
102
+ /**
103
+ * Insert a record, or update the existing one matching `match`.
104
+ *
105
+ * The server decides: exactly one match updates it, no match inserts a new
106
+ * record (seeded with the `match` fields), more than one match is an error.
107
+ * Returns the resulting record and a `created` flag (true = inserted, false
108
+ * = updated).
109
+ *
110
+ * Online-only by design. Unlike `insert`, an upsert is NOT queued offline:
111
+ * the insert-vs-update decision needs the server's authoritative view of
112
+ * what already exists, so deferring it could create a duplicate or apply a
113
+ * wrong update on later sync. It throws on network failure instead. A raw
114
+ * fetch is used (not `request`) so the status code is readable: 201 =
115
+ * created, 200 = updated.
116
+ */
117
+ async upsert(collection, match, data) {
118
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/upsert`, {
119
+ method: 'POST',
120
+ headers: this.headers,
121
+ body: JSON.stringify({ collection, match, data }),
122
+ });
123
+ const body = await res.json();
124
+ if (!res.ok) {
125
+ if (res.status === 409) {
126
+ throw new database_errors_1.KoolbaseConflictError(body.error);
127
+ }
128
+ throw new Error(body.error ?? `Upsert failed: ${res.status}`);
129
+ }
130
+ const created = res.status === 201;
131
+ const record = (0, record_1.recordFromWire)(body);
132
+ // Keep the cache fresh, same intent as insert's post-success invalidate.
133
+ const userId = this.getUserId() ?? 'anonymous';
134
+ await (0, cache_store_1.invalidateCache)(userId, collection);
135
+ return { record, created };
136
+ }
137
+ // ─── Delete where (online-only) ─────────────────────────────────────────────
138
+ /**
139
+ * Bulk-delete every record in `collection` matching `filters`.
140
+ *
141
+ * The server applies the collection's delete rule (scoping to the caller for
142
+ * owner/scoped rules) and returns the number of records deleted.
143
+ *
144
+ * Online-only by design — like upsert, this is NOT queued offline: a bulk
145
+ * delete needs the server's authoritative view of what matches, so it throws
146
+ * on network failure rather than risk deleting the wrong set on later sync.
147
+ * The collection cache is invalidated on success.
148
+ */
149
+ async deleteWhere(collection, filters) {
150
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/delete-where`, {
151
+ method: 'POST',
152
+ headers: this.headers,
153
+ body: JSON.stringify({ collection, filters }),
154
+ });
155
+ const body = await res.json();
156
+ if (!res.ok) {
157
+ throw new Error(body.error ?? `Delete failed: ${res.status}`);
158
+ }
159
+ const userId = this.getUserId() ?? 'anonymous';
160
+ await (0, cache_store_1.invalidateCache)(userId, collection);
161
+ return body.deleted ?? 0;
162
+ }
100
163
  // ─── Get single record ──────────────────────────────────────────────────────
101
164
  async get(recordId) {
102
165
  const raw = await this.request('GET', `/v1/sdk/db/records/${recordId}`);
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/dist/types.d.ts CHANGED
@@ -130,6 +130,10 @@ export interface QueryResult {
130
130
  total: number;
131
131
  isFromCache?: boolean;
132
132
  }
133
+ export interface UpsertResult {
134
+ record: KoolbaseRecord;
135
+ created: boolean;
136
+ }
133
137
  export interface PendingWrite {
134
138
  id: string;
135
139
  type: 'insert' | 'update' | 'delete';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "2.0.0",
3
+ "version": "2.2.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",