@techfinityedge/koolbase-react-native 1.11.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,9 @@ 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@^1.10.0
19
+ npm install @techfinityedge/koolbase-react-native@^2.0.0
20
20
  # or
21
- yarn add @techfinityedge/koolbase-react-native@^1.10.0
21
+ yarn add @techfinityedge/koolbase-react-native@^2.0.0
22
22
  ```
23
23
 
24
24
  **4. Initialize at app startup:**
@@ -147,6 +147,11 @@ const { records } = await Koolbase.db.query('posts', {
147
147
  orderDesc: true,
148
148
  });
149
149
 
150
+ // Read fields off a record
151
+ const post = records[0];
152
+ console.log(post.data.title); // your fields live under .data
153
+ console.log(post.id, post.collection); // metadata
154
+
150
155
  // Populate related records
151
156
  const { records: postsWithAuthor } = await Koolbase.db.query('posts', {
152
157
  populate: ['author_id:users'],
@@ -157,6 +162,38 @@ await Koolbase.db.update('record-id', { title: 'Updated' });
157
162
  await Koolbase.db.delete('record-id');
158
163
  ```
159
164
 
165
+ ### Upsert
166
+
167
+ Insert a record, or update the existing one matching a filter.
168
+
169
+ \`\`\`ts
170
+ const result = await Koolbase.db.upsert(
171
+ 'profiles',
172
+ { user_id: userId },
173
+ { weightKg: 70 }
174
+ );
175
+
176
+ console.log(result.created); // true if inserted, false if updated
177
+ console.log(result.record.id);
178
+ \`\`\`
179
+
180
+ > Online-only: needs the server's view to decide insert vs update, so unlike
181
+ > `insert` it isn't queued offline and throws on network failure.
182
+
183
+ ### Delete where
184
+
185
+ Bulk-delete every record matching a filter. Returns the number deleted.
186
+
187
+ \`\`\`ts
188
+ const deleted = await Koolbase.db.deleteWhere('sessions', {
189
+ user_id: userId,
190
+ status: 'expired',
191
+ });
192
+ \`\`\`
193
+
194
+ > A non-empty filter is required. The collection's delete rule applies; for
195
+ > `owner`/`scoped` rules the delete is scoped to your own records. Online-only.
196
+
160
197
  ### Offline-first
161
198
 
162
199
  ```typescript
@@ -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;
@@ -6,8 +6,37 @@ export declare class KoolbaseDatabase {
6
6
  constructor(config: KoolbaseConfig, getUserId: () => string | null);
7
7
  private get headers();
8
8
  private request;
9
+ private runQuery;
9
10
  query(collection: string, options?: QueryOptions): Promise<QueryResult>;
10
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>;
11
40
  get(recordId: string): Promise<KoolbaseRecord>;
12
41
  update(recordId: string, data: Record<string, unknown>): Promise<KoolbaseRecord>;
13
42
  delete(recordId: string): Promise<void>;
package/dist/database.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KoolbaseDatabase = void 0;
4
4
  const cache_store_1 = require("./cache-store");
5
5
  const sync_engine_1 = require("./sync-engine");
6
+ const record_1 = require("./record");
6
7
  function generateId() {
7
8
  return 'local_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
8
9
  }
@@ -34,13 +35,8 @@ class KoolbaseDatabase {
34
35
  return data;
35
36
  }
36
37
  // ─── Query (cache-first) ───────────────────────────────────────────────────
37
- async query(collection, options = {}) {
38
- const userId = this.getUserId() ?? 'anonymous';
39
- const queryHash = (0, cache_store_1.hashQuery)(collection, options);
40
- // 1. Return cached data immediately if available
41
- const cached = await (0, cache_store_1.getCached)(userId, collection, queryHash);
42
- // 2. Fetch from network in background
43
- this.request('POST', '/v1/sdk/db/query', {
38
+ async runQuery(collection, options) {
39
+ const raw = await this.request('POST', '/v1/sdk/db/query', {
44
40
  collection,
45
41
  filters: options.filters ?? {},
46
42
  limit: options.limit ?? 20,
@@ -48,26 +44,22 @@ class KoolbaseDatabase {
48
44
  order_by: options.orderBy,
49
45
  order_desc: options.orderDesc ?? false,
50
46
  populate: options.populate ?? [],
51
- })
52
- .then(async (result) => {
53
- await (0, cache_store_1.setCached)(userId, collection, queryHash, result);
54
- })
47
+ });
48
+ return { records: raw.records.map(record_1.recordFromWire), total: raw.total };
49
+ }
50
+ async query(collection, options = {}) {
51
+ const userId = this.getUserId() ?? 'anonymous';
52
+ const queryHash = (0, cache_store_1.hashQuery)(collection, options);
53
+ const cached = await (0, cache_store_1.getCached)(userId, collection, queryHash);
54
+ this.runQuery(collection, options)
55
+ .then(result => (0, cache_store_1.setCached)(userId, collection, queryHash, result))
55
56
  .catch(() => {
56
- // Network unavailable — cached data is already returned
57
+ // Network unavailable — cached data already returned
57
58
  });
58
59
  if (cached) {
59
60
  return { ...cached, isFromCache: true };
60
61
  }
61
- // No cache wait for network
62
- const result = await this.request('POST', '/v1/sdk/db/query', {
63
- collection,
64
- filters: options.filters ?? {},
65
- limit: options.limit ?? 20,
66
- offset: options.offset ?? 0,
67
- order_by: options.orderBy,
68
- order_desc: options.orderDesc ?? false,
69
- populate: options.populate ?? [],
70
- });
62
+ const result = await this.runQuery(collection, options);
71
63
  await (0, cache_store_1.setCached)(userId, collection, queryHash, result);
72
64
  return { ...result, isFromCache: false };
73
65
  }
@@ -77,8 +69,6 @@ class KoolbaseDatabase {
77
69
  // Build optimistic record
78
70
  const optimisticRecord = {
79
71
  id: generateId(),
80
- projectId: '',
81
- collectionId: '',
82
72
  createdBy: userId,
83
73
  data,
84
74
  createdAt: new Date().toISOString(),
@@ -107,31 +97,86 @@ class KoolbaseDatabase {
107
97
  });
108
98
  return optimisticRecord;
109
99
  }
100
+ // ─── Upsert (online-only) ─────────────────────────────────────────────────
101
+ /**
102
+ * Insert a record, or update the existing one matching `match`.
103
+ *
104
+ * The server decides: exactly one match updates it, no match inserts a new
105
+ * record (seeded with the `match` fields), more than one match is an error.
106
+ * Returns the resulting record and a `created` flag (true = inserted, false
107
+ * = updated).
108
+ *
109
+ * Online-only by design. Unlike `insert`, an upsert is NOT queued offline:
110
+ * the insert-vs-update decision needs the server's authoritative view of
111
+ * what already exists, so deferring it could create a duplicate or apply a
112
+ * wrong update on later sync. It throws on network failure instead. A raw
113
+ * fetch is used (not `request`) so the status code is readable: 201 =
114
+ * created, 200 = updated.
115
+ */
116
+ async upsert(collection, match, data) {
117
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/upsert`, {
118
+ method: 'POST',
119
+ headers: this.headers,
120
+ body: JSON.stringify({ collection, match, data }),
121
+ });
122
+ const body = await res.json();
123
+ if (!res.ok) {
124
+ throw new Error(body.error ?? `Upsert failed: ${res.status}`);
125
+ }
126
+ const created = res.status === 201;
127
+ const record = (0, record_1.recordFromWire)(body);
128
+ // Keep the cache fresh, same intent as insert's post-success invalidate.
129
+ const userId = this.getUserId() ?? 'anonymous';
130
+ await (0, cache_store_1.invalidateCache)(userId, collection);
131
+ return { record, created };
132
+ }
133
+ // ─── Delete where (online-only) ─────────────────────────────────────────────
134
+ /**
135
+ * Bulk-delete every record in `collection` matching `filters`.
136
+ *
137
+ * The server applies the collection's delete rule (scoping to the caller for
138
+ * owner/scoped rules) and returns the number of records deleted.
139
+ *
140
+ * Online-only by design — like upsert, this is NOT queued offline: a bulk
141
+ * delete needs the server's authoritative view of what matches, so it throws
142
+ * on network failure rather than risk deleting the wrong set on later sync.
143
+ * The collection cache is invalidated on success.
144
+ */
145
+ async deleteWhere(collection, filters) {
146
+ const res = await fetch(`${this.config.baseUrl}/v1/sdk/db/delete-where`, {
147
+ method: 'POST',
148
+ headers: this.headers,
149
+ body: JSON.stringify({ collection, filters }),
150
+ });
151
+ const body = await res.json();
152
+ if (!res.ok) {
153
+ throw new Error(body.error ?? `Delete failed: ${res.status}`);
154
+ }
155
+ const userId = this.getUserId() ?? 'anonymous';
156
+ await (0, cache_store_1.invalidateCache)(userId, collection);
157
+ return body.deleted ?? 0;
158
+ }
110
159
  // ─── Get single record ──────────────────────────────────────────────────────
111
160
  async get(recordId) {
112
- return this.request('GET', `/v1/sdk/db/records/${recordId}`);
161
+ const raw = await this.request('GET', `/v1/sdk/db/records/${recordId}`);
162
+ return (0, record_1.recordFromWire)(raw);
113
163
  }
114
164
  // ─── Update ─────────────────────────────────────────────────────────────────
115
165
  async update(recordId, data) {
116
166
  const userId = this.getUserId() ?? 'anonymous';
117
- // Add to write queue
118
167
  await (0, cache_store_1.addToWriteQueue)(userId, {
119
168
  id: generateId(),
120
169
  type: 'update',
121
170
  recordId,
122
171
  data,
123
172
  });
124
- // Try network
125
173
  try {
126
- const result = await this.request('PATCH', `/v1/sdk/db/records/${recordId}`, { data });
127
- return result;
174
+ const raw = await this.request('PATCH', `/v1/sdk/db/records/${recordId}`, { data });
175
+ return (0, record_1.recordFromWire)(raw);
128
176
  }
129
177
  catch {
130
- // Return optimistic version
131
178
  return {
132
179
  id: recordId,
133
- projectId: '',
134
- collectionId: '',
135
180
  data,
136
181
  createdAt: '',
137
182
  updatedAt: new Date().toISOString(),
package/dist/realtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KoolbaseRealtime = void 0;
4
+ const record_1 = require("./record");
4
5
  class KoolbaseRealtime {
5
6
  constructor(config) {
6
7
  this.ws = null;
@@ -31,7 +32,14 @@ class KoolbaseRealtime {
31
32
  this.ws = new WebSocket(`${wsUrl}/v1/sdk/realtime?key=${this.config.publicKey}`);
32
33
  this.ws.onmessage = (event) => {
33
34
  try {
34
- const msg = JSON.parse(event.data);
35
+ const raw = JSON.parse(event.data);
36
+ if (!raw || !raw.record)
37
+ return;
38
+ const msg = {
39
+ type: raw.type,
40
+ collection: raw.collection,
41
+ record: (0, record_1.recordFromWire)(raw.record),
42
+ };
35
43
  const callbacks = this.listeners.get(msg.collection) ?? [];
36
44
  callbacks.forEach((cb) => cb(msg));
37
45
  }
@@ -0,0 +1,2 @@
1
+ import { KoolbaseRecord } from './types';
2
+ export declare function recordFromWire(raw: Record<string, unknown>): KoolbaseRecord;
package/dist/record.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordFromWire = recordFromWire;
4
+ // Converts the flat public wire shape into a KoolbaseRecord.
5
+ // Server sends: { $id, $createdAt, $updatedAt, $collection, $createdBy?, ...fields }
6
+ function recordFromWire(raw) {
7
+ const data = {};
8
+ for (const key of Object.keys(raw)) {
9
+ if (!key.startsWith('$'))
10
+ data[key] = raw[key];
11
+ }
12
+ return {
13
+ id: raw['$id'],
14
+ collection: raw['$collection'],
15
+ createdBy: raw['$createdBy'],
16
+ data,
17
+ createdAt: raw['$createdAt'],
18
+ updatedAt: raw['$updatedAt'],
19
+ };
20
+ }
package/dist/types.d.ts CHANGED
@@ -111,8 +111,7 @@ export interface PhoneVerifyResult {
111
111
  }
112
112
  export interface KoolbaseRecord {
113
113
  id: string;
114
- projectId: string;
115
- collectionId: string;
114
+ collection?: string;
116
115
  createdBy?: string;
117
116
  data: Record<string, unknown>;
118
117
  createdAt: string;
@@ -131,6 +130,10 @@ export interface QueryResult {
131
130
  total: number;
132
131
  isFromCache?: boolean;
133
132
  }
133
+ export interface UpsertResult {
134
+ record: KoolbaseRecord;
135
+ created: boolean;
136
+ }
134
137
  export interface PendingWrite {
135
138
  id: string;
136
139
  type: 'insert' | 'update' | 'delete';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techfinityedge/koolbase-react-native",
3
- "version": "1.11.0",
3
+ "version": "2.1.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",