nexabase-console 2.0.7 → 2.0.9

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.
@@ -5,6 +5,7 @@ const SnapshotManager_1 = require("./SnapshotManager");
5
5
  const DocumentReference_1 = require("./DocumentReference");
6
6
  const NexaError_1 = require("../errors/NexaError");
7
7
  const helpers_1 = require("../utils/helpers");
8
+ const QueryUtils_1 = require("./QueryUtils");
8
9
  const query = (queryObject, ...queryConstraints) => {
9
10
  const q = {
10
11
  type: 'query',
@@ -13,85 +14,137 @@ const query = (queryObject, ...queryConstraints) => {
13
14
  constraints: queryObject.type === 'query' ? [...(queryObject.constraints || [])] : []
14
15
  };
15
16
  q.constraints = [...(q.constraints || []), ...queryConstraints];
17
+ // Basic client-side validation
18
+ for (const c of q.constraints) {
19
+ if (c.type === 'limit' && (!Number.isSafeInteger(c.limitValue) || c.limitValue <= 0)) {
20
+ throw Object.assign(new Error('Limit must be a positive safe integer.'), { code: 'invalid-argument' });
21
+ }
22
+ if (['in', 'not-in', 'array-contains-any'].includes(c.opStr || '') && (!Array.isArray(c.value) || c.value.length === 0)) {
23
+ throw Object.assign(new Error(`Invalid value for ${c.opStr}: must be a non-empty array.`), { code: 'invalid-argument' });
24
+ }
25
+ }
16
26
  return q;
17
27
  };
18
28
  exports.query = query;
19
- const getCachedDocs = (queryOrCollection) => {
29
+ const getCachedDocs = (queryOrCollection, fromCache = true) => {
20
30
  const db = queryOrCollection.db;
21
31
  const collectionPath = queryOrCollection.path;
22
32
  const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
23
33
  const results = [];
24
- Object.keys(db._firestoreCache).forEach((p) => {
25
- if (p.startsWith(collectionPath + '/')) {
34
+ // Local evaluation over cache
35
+ for (const p of Object.keys(db._firestoreCache)) {
36
+ if ((0, QueryUtils_1.isDirectChildDocument)(collectionPath, p)) {
26
37
  const parts = p.split('/');
27
38
  const docId = parts[parts.length - 1];
28
39
  const docRef = (0, DocumentReference_1.doc)(db, `${collectionPath}/${docId}`);
29
- const cachedData = db._firestoreCache[p];
40
+ const cachedData = (0, QueryUtils_1.cloneNexaData)(db._firestoreCache[p]);
30
41
  results.push({
31
42
  id: docId,
32
43
  ref: docRef,
33
- metadata: { fromCache: true, hasPendingWrites: db.hasPendingWrites(docRef.path) },
44
+ metadata: { fromCache, hasPendingWrites: db.hasPendingWrites(docRef.path) },
34
45
  exists: () => true,
35
46
  data: () => cachedData,
36
47
  get: (fieldPath) => (0, helpers_1.getNestedValue)(cachedData, fieldPath, docId)
37
48
  });
38
49
  }
39
- });
50
+ }
40
51
  let filtered = results;
41
52
  let limitCount = undefined;
53
+ const orderBys = [];
54
+ for (const c of constraints) {
55
+ if (c.type === 'orderBy')
56
+ orderBys.push(c);
57
+ }
58
+ // Add implicit tie-breaker by document ID
59
+ orderBys.push({ type: 'orderBy', fieldPath: '__name__', directionStr: 'asc' });
42
60
  for (const c of constraints) {
43
61
  if (c.type === 'where' && c.fieldPath && c.opStr) {
44
62
  filtered = filtered.filter((docItem) => {
45
63
  const val = (0, helpers_1.getNestedValue)(docItem.data(), c.fieldPath, docItem.id);
64
+ if (val === undefined) {
65
+ // Missing fields should not match inequalities by default
66
+ if (['!=', 'not-in', '<', '<=', '>', '>='].includes(c.opStr))
67
+ return false;
68
+ }
46
69
  if (c.opStr === '==')
47
- return val === c.value;
70
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) === 0;
48
71
  if (c.opStr === '>')
49
- return val > c.value;
72
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) > 0;
50
73
  if (c.opStr === '<')
51
- return val < c.value;
74
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) < 0;
52
75
  if (c.opStr === '>=')
53
- return val >= c.value;
76
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) >= 0;
54
77
  if (c.opStr === '<=')
55
- return val <= c.value;
78
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) <= 0;
56
79
  if (c.opStr === '!=')
57
- return val !== c.value;
80
+ return (0, QueryUtils_1.compareNexaValues)(val, c.value) !== 0;
58
81
  if (c.opStr === 'in')
59
- return Array.isArray(c.value) && c.value.includes(val);
82
+ return Array.isArray(c.value) && c.value.some((v) => (0, QueryUtils_1.compareNexaValues)(v, val) === 0);
60
83
  if (c.opStr === 'not-in')
61
- return Array.isArray(c.value) && !c.value.includes(val);
84
+ return Array.isArray(c.value) && !c.value.some((v) => (0, QueryUtils_1.compareNexaValues)(v, val) === 0);
62
85
  if (c.opStr === 'array-contains')
63
- return Array.isArray(val) && val.includes(c.value);
86
+ return Array.isArray(val) && val.some((v) => (0, QueryUtils_1.compareNexaValues)(v, c.value) === 0);
64
87
  if (c.opStr === 'array-contains-any')
65
- return Array.isArray(val) && Array.isArray(c.value) && c.value.some((v) => val.includes(v));
66
- return true;
67
- });
68
- }
69
- else if (c.type === 'orderBy' && c.fieldPath) {
70
- filtered = [...filtered].sort((a, b) => {
71
- const valA = (0, helpers_1.getNestedValue)(a.data(), c.fieldPath, a.id);
72
- const valB = (0, helpers_1.getNestedValue)(b.data(), c.fieldPath, b.id);
73
- if (valA < valB)
74
- return c.directionStr === 'desc' ? 1 : -1;
75
- if (valA > valB)
76
- return c.directionStr === 'desc' ? -1 : 1;
77
- return 0;
88
+ return Array.isArray(val) && Array.isArray(c.value) && c.value.some((cVal) => val.some((v) => (0, QueryUtils_1.compareNexaValues)(v, cVal) === 0));
89
+ throw new Error(`Unsupported operator: ${c.opStr}`);
78
90
  });
79
91
  }
80
92
  }
93
+ // Documents without orderBy field must be excluded
94
+ filtered = filtered.filter((docItem) => {
95
+ for (const ob of orderBys) {
96
+ if (ob.fieldPath !== '__name__') {
97
+ const val = (0, helpers_1.getNestedValue)(docItem.data(), ob.fieldPath, docItem.id);
98
+ if (val === undefined)
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ });
104
+ filtered = [...filtered].sort((a, b) => {
105
+ for (const ob of orderBys) {
106
+ const field = ob.fieldPath === '__name__' ? null : ob.fieldPath;
107
+ const valA = field ? (0, helpers_1.getNestedValue)(a.data(), field, a.id) : a.id;
108
+ const valB = field ? (0, helpers_1.getNestedValue)(b.data(), field, b.id) : b.id;
109
+ const comp = (0, QueryUtils_1.compareNexaValues)(valA, valB);
110
+ if (comp !== 0) {
111
+ return ob.directionStr === 'desc' ? -comp : comp;
112
+ }
113
+ }
114
+ return 0;
115
+ });
81
116
  for (const c of constraints) {
82
117
  if (['startAt', 'startAfter', 'endAt', 'endBefore'].includes(c.type)) {
83
118
  const isSnapshot = c.docSnapshot && typeof c.docSnapshot === 'object' && 'id' in c.docSnapshot;
84
- const cursorIndex = filtered.findIndex((d) => (isSnapshot ? d.id === c.docSnapshot.id : d.id === c.docSnapshot));
85
- if (cursorIndex !== -1) {
86
- if (c.type === 'startAt')
87
- filtered = filtered.slice(cursorIndex);
88
- if (c.type === 'startAfter')
89
- filtered = filtered.slice(cursorIndex + 1);
90
- if (c.type === 'endAt')
91
- filtered = filtered.slice(0, cursorIndex + 1);
92
- if (c.type === 'endBefore')
93
- filtered = filtered.slice(0, cursorIndex);
94
- }
119
+ const cursorVals = isSnapshot
120
+ ? orderBys.map(ob => {
121
+ if (ob.fieldPath === '__name__')
122
+ return c.docSnapshot.id;
123
+ return typeof c.docSnapshot.get === 'function'
124
+ ? c.docSnapshot.get(ob.fieldPath)
125
+ : (0, helpers_1.getNestedValue)(c.docSnapshot.data ? c.docSnapshot.data() : c.docSnapshot, ob.fieldPath, c.docSnapshot.id);
126
+ })
127
+ : (Array.isArray(c.cursorValues) ? c.cursorValues : [c.cursorValues]);
128
+ filtered = filtered.filter(d => {
129
+ for (let i = 0; i < Math.min(orderBys.length, cursorVals.length); i++) {
130
+ const field = orderBys[i].fieldPath === '__name__' ? null : orderBys[i].fieldPath;
131
+ const dVal = field ? (0, helpers_1.getNestedValue)(d.data(), field, d.id) : d.id;
132
+ const cVal = cursorVals[i];
133
+ const comp = (0, QueryUtils_1.compareNexaValues)(dVal, cVal);
134
+ if (comp !== 0) {
135
+ const dir = orderBys[i].directionStr === 'desc' ? -1 : 1;
136
+ const adjustedComp = comp * dir;
137
+ if (c.type === 'startAt' || c.type === 'startAfter')
138
+ return adjustedComp > 0;
139
+ if (c.type === 'endAt' || c.type === 'endBefore')
140
+ return adjustedComp < 0;
141
+ }
142
+ }
143
+ // If all evaluated fields are equal
144
+ if (c.type === 'startAt' || c.type === 'endAt')
145
+ return true;
146
+ return false;
147
+ });
95
148
  }
96
149
  else if (c.type === 'limit' && typeof c.limitValue === 'number') {
97
150
  limitCount = c.limitValue;
@@ -100,7 +153,7 @@ const getCachedDocs = (queryOrCollection) => {
100
153
  if (limitCount !== undefined) {
101
154
  filtered = filtered.slice(0, limitCount);
102
155
  }
103
- return SnapshotManager_1.SnapshotManager.createQuerySnapshot(filtered, true, db.hasPendingWrites(collectionPath));
156
+ return SnapshotManager_1.SnapshotManager.createQuerySnapshot(filtered, fromCache, filtered.some(d => d.metadata.hasPendingWrites) || db.hasPendingWrites(collectionPath));
104
157
  };
105
158
  exports.getCachedDocs = getCachedDocs;
106
159
  const getDocs = async (queryOrCollection) => {
@@ -109,68 +162,72 @@ const getDocs = async (queryOrCollection) => {
109
162
  await db._initPromise;
110
163
  const collectionPath = queryOrCollection.path;
111
164
  const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
165
+ const serializedConstraints = constraints.map(QueryUtils_1.serializeConstraint);
112
166
  if (db.cacheStrategy === 'cache-first') {
113
- const cached = (0, exports.getCachedDocs)(queryOrCollection);
114
- if (cached && !cached.empty) {
167
+ const cached = (0, exports.getCachedDocs)(queryOrCollection, true);
168
+ if (cached && (db._queryCacheMetadata[collectionPath] || !cached.empty)) {
169
+ // background refresh
115
170
  db.client
116
- .post(`/api/firestore/${db.projectId}/query`, { collectionPath, constraints })
171
+ .post(`/api/firestore/${db.projectId}/query`, { collectionPath, constraints: serializedConstraints })
117
172
  .then(async (res) => {
118
- const docs = res.data.documents || [];
119
- const serverIds = new Set(docs.map((d) => d.id));
120
- for (const d of docs) {
121
- await db._setCache(`${collectionPath}/${d.id}`, d.fields);
122
- }
123
- if (constraints.length === 0) {
124
- for (const cachePath of Object.keys(db._firestoreCache)) {
125
- if (cachePath.startsWith(collectionPath + '/')) {
126
- const docId = cachePath.split('/').pop();
127
- if (docId && !serverIds.has(docId)) {
128
- await db._deleteCache(cachePath);
129
- }
130
- }
131
- }
132
- }
173
+ const docs = res.data?.documents;
174
+ if (!Array.isArray(docs))
175
+ return;
176
+ const complete = res.data?.complete === true;
177
+ await db._commitAtomicQueryResults({
178
+ queryKey: collectionPath,
179
+ collectionPath,
180
+ serverDocs: docs,
181
+ isComplete: complete,
182
+ constraints: serializedConstraints
183
+ });
184
+ db._queryCacheMetadata[collectionPath] = true;
133
185
  db._notifySnapshotCallbacks(collectionPath, 'bulk_update', null);
134
186
  })
135
- .catch(() => { });
187
+ .catch((err) => {
188
+ console.error('[NexaBase] Background query refresh failed:', err);
189
+ });
136
190
  return cached;
137
191
  }
138
192
  }
139
193
  try {
140
- const res = await db.client.post(`/api/firestore/${db.projectId}/query`, { collectionPath, constraints });
141
- const docs = res.data.documents || [];
142
- const serverIds = new Set(docs.map((d) => d.id));
143
- for (const d of docs) {
144
- await db._setCache(`${collectionPath}/${d.id}`, d.fields);
194
+ const res = await db.client.post(`/api/firestore/${db.projectId}/query`, { collectionPath, constraints: serializedConstraints });
195
+ const docs = res.data?.documents;
196
+ if (!Array.isArray(docs)) {
197
+ throw Object.assign(new Error('Invalid server response: documents must be an array'), { code: 'internal' });
145
198
  }
146
- if (constraints.length === 0) {
147
- for (const cachePath of Object.keys(db._firestoreCache)) {
148
- if (cachePath.startsWith(collectionPath + '/')) {
149
- const docId = cachePath.split('/').pop();
150
- if (docId && !serverIds.has(docId)) {
151
- await db._deleteCache(cachePath);
152
- }
153
- }
199
+ const seenIds = new Set();
200
+ const validDocs = [];
201
+ for (const d of docs) {
202
+ if (d && typeof d.id === 'string' && d.fields && typeof d.fields === 'object' && !seenIds.has(d.id)) {
203
+ seenIds.add(d.id);
204
+ validDocs.push(d);
154
205
  }
155
206
  }
156
- const docsArray = docs.map((d) => {
157
- const docRef = (0, DocumentReference_1.doc)(db, `${collectionPath}/${d.id}`);
158
- return {
159
- id: d.id,
160
- ref: docRef,
161
- metadata: { fromCache: false, hasPendingWrites: db.hasPendingWrites(docRef.path) },
162
- exists: () => true,
163
- data: () => d.fields,
164
- get: (fieldPath) => (0, helpers_1.getNestedValue)(d.fields, fieldPath, d.id)
165
- };
207
+ const complete = res.data?.complete === true;
208
+ // Apply results while preserving pending mutations safely
209
+ await db._commitAtomicQueryResults({
210
+ queryKey: collectionPath,
211
+ collectionPath,
212
+ serverDocs: validDocs,
213
+ isComplete: complete,
214
+ constraints: serializedConstraints
166
215
  });
167
- return SnapshotManager_1.SnapshotManager.createQuerySnapshot(docsArray, false, db.hasPendingWrites(collectionPath));
216
+ db._queryCacheMetadata[collectionPath] = true;
217
+ // After persistence, read the newly reconciled local state
218
+ return (0, exports.getCachedDocs)(queryOrCollection, false);
168
219
  }
169
220
  catch (err) {
170
- if (err && err.response && err.response.status < 500) {
171
- throw (0, NexaError_1.toNexaError)(err);
221
+ const status = err?.response?.status;
222
+ const code = err?.code || 'internal';
223
+ const isRetryable = status
224
+ ? [408, 425, 429, 500, 502, 503, 504].includes(status)
225
+ : ['network-error', 'timeout', 'unavailable', 'aborted', 'resource-exhausted'].includes(code);
226
+ if (isRetryable) {
227
+ // Return cache if it's just a network disconnect/retryable error
228
+ return (0, exports.getCachedDocs)(queryOrCollection, true);
172
229
  }
173
- return (0, exports.getCachedDocs)(queryOrCollection);
230
+ throw (0, NexaError_1.toNexaError)(err);
174
231
  }
175
232
  };
176
233
  exports.getDocs = getDocs;
@@ -187,51 +244,45 @@ const average = (field) => {
187
244
  };
188
245
  exports.average = average;
189
246
  const getAggregateFromServer = async (queryOrCollection, aggregateSpec) => {
190
- const querySnapshot = await (0, exports.getDocs)(queryOrCollection);
191
- const docs = querySnapshot.docs;
192
- const resultData = {};
247
+ const db = queryOrCollection.db;
248
+ if (db._initPromise)
249
+ await db._initPromise;
250
+ const collectionPath = queryOrCollection.path;
251
+ const constraints = 'constraints' in queryOrCollection ? queryOrCollection.constraints || [] : [];
252
+ const serializedSpec = {};
193
253
  for (const [key, spec] of Object.entries(aggregateSpec)) {
194
- if (spec.type === 'count') {
195
- resultData[key] = docs.length;
254
+ if (key === '__proto__' || key === 'constructor')
255
+ continue;
256
+ let fp = spec.fieldPath;
257
+ if (fp && typeof fp === 'object' && 'getPathString' in fp && typeof fp.getPathString === 'function') {
258
+ fp = fp.getPathString();
196
259
  }
197
- else if (spec.type === 'sum') {
198
- const field = spec.fieldPath || '';
199
- let sumVal = 0;
200
- for (const d of docs) {
201
- const val = (0, helpers_1.getNestedValue)(d.data(), field, d.id);
202
- if (typeof val === 'number' && !isNaN(val)) {
203
- sumVal += val;
204
- }
205
- }
206
- resultData[key] = sumVal;
207
- }
208
- else if (spec.type === 'average') {
209
- const field = spec.fieldPath || '';
210
- let sumVal = 0;
211
- let countVal = 0;
212
- for (const d of docs) {
213
- const val = (0, helpers_1.getNestedValue)(d.data(), field, d.id);
214
- if (typeof val === 'number' && !isNaN(val)) {
215
- sumVal += val;
216
- countVal++;
217
- }
218
- }
219
- resultData[key] = countVal > 0 ? sumVal / countVal : null;
260
+ serializedSpec[key] = {
261
+ type: spec.type,
262
+ fieldPath: typeof fp === 'string' ? fp : undefined
263
+ };
264
+ }
265
+ try {
266
+ const res = await db.client.post(`/api/firestore/${db.projectId}/aggregate`, {
267
+ collectionPath,
268
+ constraints: constraints.map(QueryUtils_1.serializeConstraint),
269
+ aggregateSpec: serializedSpec
270
+ });
271
+ if (!res.data || typeof res.data.result !== 'object' || res.data.result === null) {
272
+ throw Object.assign(new Error('Invalid aggregate server response'), { code: 'internal' });
220
273
  }
274
+ return {
275
+ type: 'AggregateQuerySnapshot',
276
+ query: queryOrCollection,
277
+ data: () => (0, QueryUtils_1.cloneNexaData)(res.data.result)
278
+ };
279
+ }
280
+ catch (err) {
281
+ throw (0, NexaError_1.toNexaError)(err);
221
282
  }
222
- return {
223
- type: 'AggregateQuerySnapshot',
224
- query: queryOrCollection,
225
- data: () => resultData
226
- };
227
283
  };
228
284
  exports.getAggregateFromServer = getAggregateFromServer;
229
285
  const getCountFromServer = async (queryOrCollection) => {
230
- const querySnapshot = await (0, exports.getDocs)(queryOrCollection);
231
- return {
232
- type: 'AggregateQuerySnapshot',
233
- query: queryOrCollection,
234
- data: () => ({ count: querySnapshot.docs.length })
235
- };
286
+ return (0, exports.getAggregateFromServer)(queryOrCollection, { count: (0, exports.count)() });
236
287
  };
237
288
  exports.getCountFromServer = getCountFromServer;
@@ -0,0 +1,5 @@
1
+ export declare function isDirectChildDocument(collectionPath: string, documentPath: string): boolean;
2
+ export declare function reviveNexaData(data: any): any;
3
+ export declare function cloneNexaData(data: any): any;
4
+ export declare function compareNexaValues(a: any, b: any): number;
5
+ export declare function serializeConstraint(c: any): any;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isDirectChildDocument = isDirectChildDocument;
4
+ exports.reviveNexaData = reviveNexaData;
5
+ exports.cloneNexaData = cloneNexaData;
6
+ exports.compareNexaValues = compareNexaValues;
7
+ exports.serializeConstraint = serializeConstraint;
8
+ const Timestamp_1 = require("./Timestamp");
9
+ function isDirectChildDocument(collectionPath, documentPath) {
10
+ if (!documentPath.startsWith(`${collectionPath}/`))
11
+ return false;
12
+ return documentPath.split('/').length === collectionPath.split('/').length + 1;
13
+ }
14
+ function reviveNexaData(data) {
15
+ if (data === null || data === undefined)
16
+ return data;
17
+ if (data instanceof Timestamp_1.Timestamp)
18
+ return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
19
+ if (Array.isArray(data))
20
+ return data.map(reviveNexaData);
21
+ if (typeof data === 'object') {
22
+ if (typeof data.isEqual === 'function' && typeof data.toMillis === 'function') {
23
+ return data;
24
+ }
25
+ // Revive timestamp
26
+ if (typeof data.seconds === 'number' && typeof data.nanoseconds === 'number' && Object.keys(data).length <= 2) {
27
+ return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
28
+ }
29
+ const cloned = {};
30
+ for (const k in data) {
31
+ if (Object.prototype.hasOwnProperty.call(data, k)) {
32
+ cloned[k] = reviveNexaData(data[k]);
33
+ }
34
+ }
35
+ return cloned;
36
+ }
37
+ return data;
38
+ }
39
+ function cloneNexaData(data) {
40
+ if (data === null || data === undefined)
41
+ return data;
42
+ if (data instanceof Timestamp_1.Timestamp)
43
+ return new Timestamp_1.Timestamp(data.seconds, data.nanoseconds);
44
+ if (Array.isArray(data))
45
+ return data.map(cloneNexaData);
46
+ if (typeof data === 'object') {
47
+ if (typeof data.isEqual === 'function' && typeof data.toMillis === 'function') {
48
+ return data; // Treat as immutable if it's our own object like Timestamp/GeoPoint
49
+ }
50
+ const cloned = {};
51
+ for (const k in data) {
52
+ if (Object.prototype.hasOwnProperty.call(data, k)) {
53
+ cloned[k] = cloneNexaData(data[k]);
54
+ }
55
+ }
56
+ return cloned;
57
+ }
58
+ return data;
59
+ }
60
+ function compareNexaValues(a, b) {
61
+ if (a === b)
62
+ return 0;
63
+ if (a === null && b !== null)
64
+ return -1;
65
+ if (a !== null && b === null)
66
+ return 1;
67
+ if (a === undefined && b !== undefined)
68
+ return -1;
69
+ if (a !== undefined && b === undefined)
70
+ return 1;
71
+ if (typeof a === 'number' && typeof b === 'number') {
72
+ if (Number.isNaN(a) && Number.isNaN(b))
73
+ return 0;
74
+ if (Number.isNaN(a))
75
+ return -1;
76
+ if (Number.isNaN(b))
77
+ return 1;
78
+ return a < b ? -1 : 1;
79
+ }
80
+ if (a?.toMillis && b?.toMillis) {
81
+ const ma = a.toMillis();
82
+ const mb = b.toMillis();
83
+ return ma < mb ? -1 : (ma > mb ? 1 : 0);
84
+ }
85
+ if (typeof a === 'string' && typeof b === 'string')
86
+ return a < b ? -1 : 1;
87
+ if (typeof a === 'boolean' && typeof b === 'boolean')
88
+ return a === b ? 0 : (a ? 1 : -1);
89
+ return 0; // Fallback
90
+ }
91
+ function serializeConstraint(c) {
92
+ const res = { type: c.type };
93
+ if (c.fieldPath) {
94
+ res.fieldPath = typeof c.fieldPath === 'string' ? c.fieldPath : c.fieldPath.getPathString();
95
+ }
96
+ if (c.opStr)
97
+ res.opStr = c.opStr;
98
+ if (c.value !== undefined)
99
+ res.value = cloneNexaData(c.value);
100
+ if (c.directionStr)
101
+ res.directionStr = c.directionStr;
102
+ if (c.limitValue !== undefined)
103
+ res.limitValue = c.limitValue;
104
+ if (c.docSnapshot) {
105
+ res.docSnapshotId = c.docSnapshot.id;
106
+ }
107
+ if (c.cursorValues) {
108
+ res.cursorValues = cloneNexaData(c.cursorValues);
109
+ }
110
+ return res;
111
+ }
@@ -1,15 +1,28 @@
1
1
  import { DocumentReference } from '../types/index';
2
2
  export { FieldValue } from './FieldValue';
3
+ export type WriteResult<T = unknown> = {
4
+ status: 'committed';
5
+ committed: true;
6
+ writtenLocally: true;
7
+ mutationId: string;
8
+ data?: T;
9
+ } | {
10
+ status: 'pending';
11
+ committed: false;
12
+ writtenLocally: true;
13
+ mutationId: string;
14
+ };
3
15
  export declare const setDoc: (docRef: DocumentReference, data: any, options?: {
4
16
  merge?: boolean;
5
17
  idempotencyKey?: string;
6
- }) => Promise<any>;
18
+ }) => Promise<WriteResult>;
7
19
  export declare const patchDoc: (docRef: DocumentReference, data: any, options?: {
8
20
  idempotencyKey?: string;
9
- }) => Promise<any>;
21
+ preconditionExists?: boolean;
22
+ }) => Promise<WriteResult>;
10
23
  export declare const updateDoc: (docRef: DocumentReference, data: any, options?: {
11
24
  idempotencyKey?: string;
12
- }) => Promise<any>;
25
+ }) => Promise<WriteResult>;
13
26
  export declare const deleteDoc: (docRef: DocumentReference, options?: {
14
27
  idempotencyKey?: string;
15
- }) => Promise<any>;
28
+ }) => Promise<WriteResult>;