@knymbus/firestoredb 1.0.2

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.
Files changed (61) hide show
  1. package/dist/FirestoreDB.d.ts +114 -0
  2. package/dist/FirestoreDB.d.ts.map +1 -0
  3. package/dist/FirestoreDB.js +407 -0
  4. package/dist/FirestoreQuery.d.ts +58 -0
  5. package/dist/FirestoreQuery.d.ts.map +1 -0
  6. package/dist/FirestoreQuery.js +200 -0
  7. package/dist/ParallelPipe.d.ts +22 -0
  8. package/dist/ParallelPipe.d.ts.map +1 -0
  9. package/dist/ParallelPipe.js +63 -0
  10. package/dist/cjs/FirestoreDB.d.ts +114 -0
  11. package/dist/cjs/FirestoreDB.d.ts.map +1 -0
  12. package/dist/cjs/FirestoreDB.js +407 -0
  13. package/dist/cjs/FirestoreQuery.d.ts +58 -0
  14. package/dist/cjs/FirestoreQuery.d.ts.map +1 -0
  15. package/dist/cjs/FirestoreQuery.js +200 -0
  16. package/dist/cjs/ParallelPipe.d.ts +22 -0
  17. package/dist/cjs/ParallelPipe.d.ts.map +1 -0
  18. package/dist/cjs/ParallelPipe.js +63 -0
  19. package/dist/cjs/index.d.ts +4 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +19 -0
  22. package/dist/cjs/types.d.ts +22 -0
  23. package/dist/cjs/types.d.ts.map +1 -0
  24. package/dist/cjs/types.js +2 -0
  25. package/dist/cjs/utils/$Operators.d.ts +4 -0
  26. package/dist/cjs/utils/$Operators.d.ts.map +1 -0
  27. package/dist/cjs/utils/$Operators.js +12 -0
  28. package/dist/cjs/utils/Hasher.d.ts +11 -0
  29. package/dist/cjs/utils/Hasher.d.ts.map +1 -0
  30. package/dist/cjs/utils/Hasher.js +28 -0
  31. package/dist/cjs/utils/LRUCache.d.ts +13 -0
  32. package/dist/cjs/utils/LRUCache.d.ts.map +1 -0
  33. package/dist/cjs/utils/LRUCache.js +39 -0
  34. package/dist/cjs/utils/hydrateDates.d.ts +6 -0
  35. package/dist/cjs/utils/hydrateDates.d.ts.map +1 -0
  36. package/dist/cjs/utils/hydrateDates.js +29 -0
  37. package/dist/cjs/utils/index.d.ts +5 -0
  38. package/dist/cjs/utils/index.d.ts.map +1 -0
  39. package/dist/cjs/utils/index.js +20 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +19 -0
  43. package/dist/types.d.ts +22 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +2 -0
  46. package/dist/utils/$Operators.d.ts +4 -0
  47. package/dist/utils/$Operators.d.ts.map +1 -0
  48. package/dist/utils/$Operators.js +12 -0
  49. package/dist/utils/Hasher.d.ts +11 -0
  50. package/dist/utils/Hasher.d.ts.map +1 -0
  51. package/dist/utils/Hasher.js +28 -0
  52. package/dist/utils/LRUCache.d.ts +13 -0
  53. package/dist/utils/LRUCache.d.ts.map +1 -0
  54. package/dist/utils/LRUCache.js +39 -0
  55. package/dist/utils/hydrateDates.d.ts +6 -0
  56. package/dist/utils/hydrateDates.d.ts.map +1 -0
  57. package/dist/utils/hydrateDates.js +29 -0
  58. package/dist/utils/index.d.ts +5 -0
  59. package/dist/utils/index.d.ts.map +1 -0
  60. package/dist/utils/index.js +20 -0
  61. package/package.json +45 -0
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FirestoreDB = void 0;
13
+ const firestore_1 = require("firebase/firestore");
14
+ const FirestoreQuery_1 = require("./FirestoreQuery");
15
+ const utils_1 = require("./utils");
16
+ class FirestoreDB {
17
+ /**
18
+ * Initialize with Firebase DB and targeted collection Name
19
+ * @param db: Firebase
20
+ * @param collection string
21
+ */
22
+ constructor(db, collectionName, options = {}) {
23
+ var _a;
24
+ /**
25
+ * FIRESTOREDB: findOne({ age: '13' }); or findOne('id_123');
26
+ */
27
+ this.findOne = (filter) => __awaiter(this, void 0, void 0, function* () {
28
+ // 1. Handle case where user passes a direct string ID
29
+ if (typeof filter === 'string') {
30
+ // We use the find() builder even for IDs to ensure the 'where("isDeleted", "==", false)'
31
+ // constraint is automatically added by the _buildConstraints helper.
32
+ const results = yield this.find({ _id: filter })
33
+ .limit(1)
34
+ .execute();
35
+ return results.length > 0 ? results[0] : null;
36
+ }
37
+ // 2. If it's an object filter, use the standard find logic
38
+ const results = yield this.find(filter)
39
+ .limit(1)
40
+ .execute();
41
+ return results.length > 0 ? results[0] : null;
42
+ });
43
+ /**
44
+ * This will count the number of documents found based on the given filter
45
+ * @param filter Object with the filter key/value pair
46
+ * @returns number
47
+ */
48
+ this.countDocuments = (...args_1) => __awaiter(this, [...args_1], void 0, function* (filter = {}) {
49
+ let q = (0, firestore_1.query)(this._collectionRef, ...this._buildConstraints(filter));
50
+ // Update the query with the incoming filter
51
+ const snapshot = yield (0, firestore_1.getCountFromServer)(q);
52
+ return snapshot.data().count;
53
+ });
54
+ this.findOneAndUpdate = (filter, updateObject) => __awaiter(this, void 0, void 0, function* () {
55
+ let docRef;
56
+ if (typeof filter === 'string') {
57
+ docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, filter);
58
+ }
59
+ else {
60
+ // if object we need to find the id first using the findOne fn
61
+ const existing = yield this.findOne(filter);
62
+ if (!existing)
63
+ throw new Error("Document not found for update");
64
+ docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, existing._id);
65
+ }
66
+ return yield (0, firestore_1.runTransaction)(this._db, (transaction) => __awaiter(this, void 0, void 0, function* () {
67
+ const docSnap = yield transaction.get(docRef);
68
+ if (!docSnap.exists())
69
+ throw new Error("Document Vanished during transaction");
70
+ transaction.update(docRef, Object.assign(Object.assign({}, updateObject), { updatedAt: (0, firestore_1.serverTimestamp)() }));
71
+ return Object.assign(Object.assign(Object.assign({ _id: docRef.id }, (docSnap.data() || {})), updateObject), { updatedAt: (0, firestore_1.serverTimestamp)() });
72
+ }));
73
+ });
74
+ /**
75
+ * NEW: find(query)
76
+ * Basic implementation. For production, you'd expand the 'filter' to handle where clauses.
77
+ */
78
+ this.find = (filter = {}) => {
79
+ return new FirestoreQuery_1.FirestoreQuery(this._db, this._collectionName, this._collectionRef, filter, this._buildConstraints.bind(this), //Pass the private helper
80
+ this.countDocuments.bind(this), // pass the count helper
81
+ this._isSoftDeleteEnabled);
82
+ };
83
+ /**
84
+ * FIRESTOREDB: insertOne(doc)
85
+ * Optimized: Uses set with merge or create
86
+ */
87
+ this.insertOne = (entity) => __awaiter(this, void 0, void 0, function* () {
88
+ const docRef = entity._id
89
+ ? (0, firestore_1.doc)(this._db, this._collectionRef.id, entity._id) //Custom ID
90
+ : (0, firestore_1.doc)(this._collectionRef); //Auto ID
91
+ // Using 'set' with { merge: false } acts like an insert/overwrite
92
+ yield (0, firestore_1.setDoc)(docRef, Object.assign(Object.assign({}, entity), { _id: docRef.id, isDeleted: false, createdAt: (0, firestore_1.serverTimestamp)(), updatedAt: (0, firestore_1.serverTimestamp)() }));
93
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
94
+ // collection are now potentially stale.
95
+ this._invalidateCache();
96
+ // 3. REFRESH: Fetch the document back to get the real server-generated timestamps
97
+ const freshSnap = yield (0, firestore_1.getDoc)(docRef);
98
+ const freshData = Object.assign({ _id: freshSnap.id }, freshSnap.data());
99
+ return (0, utils_1.hydrateDates)(freshData);
100
+ });
101
+ /**
102
+ * FIRESTOREDB: insertMany(docs) - Uses Firestore Batched Writes (Limit 500 per batch)
103
+ *
104
+ */
105
+ this.insertMany = (entities) => __awaiter(this, void 0, void 0, function* () {
106
+ const CHUNK_SIZE = 500;
107
+ const results = [];
108
+ const batchPromises = [];
109
+ // 1. Outer Loop: Creates a new batch for every 500 items
110
+ for (let i = 0; i < entities.length; i += CHUNK_SIZE) {
111
+ const batch = (0, firestore_1.writeBatch)(this._db);
112
+ const chunk = entities.slice(i, i + CHUNK_SIZE);
113
+ // Inner loop: Set the item to be created
114
+ chunk.forEach(entity => {
115
+ const docRef = entity._id
116
+ ? (0, firestore_1.doc)(this._db, this._collectionRef.id, entity._id) //Custom ID
117
+ : (0, firestore_1.doc)(this._collectionRef); //Auto ID
118
+ const finalDoc = Object.assign(Object.assign({}, entity), { _id: docRef.id, isDeleted: false, createdAt: (0, firestore_1.serverTimestamp)(), updatedAt: (0, firestore_1.serverTimestamp)() // Added
119
+ });
120
+ // Add operation to the current batch
121
+ batch.set(docRef, finalDoc);
122
+ // Track the processed doc to return to the user
123
+ results.push(finalDoc._id);
124
+ });
125
+ // Queue the batch commit
126
+ batchPromises.push(batch.commit());
127
+ }
128
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
129
+ // collection are now potentially stale.
130
+ this._invalidateCache();
131
+ // 4. Execute all batches in parallel for maximum speed
132
+ try {
133
+ yield Promise.all(batchPromises);
134
+ return results;
135
+ }
136
+ catch (error) {
137
+ console.error("FirestoreDB::insertMany: Error committing batches", error);
138
+ throw error;
139
+ }
140
+ });
141
+ /**
142
+ * FIRESTOREDB: updateOne(filter, update)
143
+ * Use "Upsert" logic when true will create a new document default to false
144
+ */
145
+ this.updateOne = (filter_1, updateObject_1, ...args_1) => __awaiter(this, [filter_1, updateObject_1, ...args_1], void 0, function* (filter, updateObject, options = {}) {
146
+ let docRef;
147
+ // 1. If it's a string, we have the direct path (Fastest)
148
+ if (typeof filter === 'string') {
149
+ docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, filter);
150
+ }
151
+ else {
152
+ // 2. If it's an object, we must find the first matching ID (Slower)
153
+ const match = yield this.findOne(filter);
154
+ // If we do not find any object we need to check if we can create a new object
155
+ if (!match) {
156
+ if (options === null || options === void 0 ? void 0 : options.upsert) {
157
+ // If upsert is true, we create a new doc with a random ID or from filter
158
+ const newDocRef = (0, firestore_1.doc)(this._collectionRef);
159
+ const initialData = Object.assign(Object.assign(Object.assign({}, this._flattenFilter), updateObject), { updatedAt: (0, firestore_1.serverTimestamp)(), createdAt: (0, firestore_1.serverTimestamp)() });
160
+ yield (0, firestore_1.setDoc)(newDocRef, initialData);
161
+ return Object.assign({ _id: newDocRef.id }, initialData);
162
+ }
163
+ return null;
164
+ }
165
+ docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, match._id);
166
+ }
167
+ // 3. Perform the update
168
+ // Using { merge: true } acts as an 'upsert' for the specific document reference
169
+ yield (0, firestore_1.setDoc)(docRef, Object.assign(Object.assign({}, updateObject), { updatedAt: (0, firestore_1.serverTimestamp)() }), { merge: true });
170
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
171
+ // collection are now potentially stale.
172
+ this._invalidateCache();
173
+ // We need to retrive the saved object and return that
174
+ const freshSnap = yield (0, firestore_1.getDoc)(docRef);
175
+ const freshData = Object.assign({ _id: freshSnap.id }, freshSnap.data() || {});
176
+ return (0, utils_1.hydrateDates)(freshData);
177
+ });
178
+ /**
179
+ * FIRESTOREDB: updateMany([ { docId: '1', entity: { status: 'A' } }, ... ])
180
+ * Optimized: Chunks updates into batches of 500 to handle large datasets.
181
+ */
182
+ this.updateMany = (updates_1, ...args_1) => __awaiter(this, [updates_1, ...args_1], void 0, function* (updates, options = { upsert: false }) {
183
+ const CHUNK_SIZE = 500;
184
+ const batchPromises = [];
185
+ const results = [];
186
+ // Math-based Chunking Loop (Jumps by 500)
187
+ for (let i = 0; i < updates.length; i += CHUNK_SIZE) {
188
+ const batch = (0, firestore_1.writeBatch)(this._db);
189
+ const chunk = updates.slice(i, i + CHUNK_SIZE);
190
+ chunk.forEach(item => {
191
+ const { docId, entity } = item;
192
+ const docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, docId);
193
+ // Inject updatedAt into the update payload
194
+ const updateData = Object.assign(Object.assign({}, entity), { updatedAt: (0, firestore_1.serverTimestamp)() });
195
+ if (options.upsert) {
196
+ // UPSERT: Create if missing, merge if exists
197
+ // We ensure _id is included in the document for consistency
198
+ // For upserts, we also need to ensure createdAt is set if the doc is new
199
+ // Firestore's 'set' with merge doesn't know if it's new, so we
200
+ // usually set createdAt only on first creation via a different method
201
+ if (!this.exists({ _id: docRef.id }))
202
+ updateData['createdAt'] = (0, firestore_1.serverTimestamp)();
203
+ batch.set(docRef, updateData, { merge: true });
204
+ // Set the Id of the object so the consumer knows which id was affected
205
+ results.push({ _id: docRef.id });
206
+ }
207
+ else {
208
+ // STRICT UPDATE: Fails if document doesn't exist (Standard Mongo)
209
+ batch.update(docRef, updateData);
210
+ }
211
+ });
212
+ // Queue the batch commit
213
+ batchPromises.push(batch.commit());
214
+ }
215
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
216
+ // collection are now potentially stale.
217
+ this._invalidateCache();
218
+ // Run all batches in parallel
219
+ try {
220
+ yield Promise.all(batchPromises);
221
+ return results;
222
+ }
223
+ catch (error) {
224
+ console.error("FirestoreDB::updateMany: Error during batched update", error);
225
+ throw error;
226
+ }
227
+ });
228
+ /**
229
+ * FIRESTOREDB: deleteOne('id_123')
230
+ */
231
+ this.deleteOne = (docId) => __awaiter(this, void 0, void 0, function* () {
232
+ const docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, docId);
233
+ // Check if it exists first if you want an accurate deletedCount
234
+ const snap = yield (0, firestore_1.getDoc)(docRef);
235
+ if (!snap.exists()) {
236
+ return { acknowledged: true, deletedCount: 0 };
237
+ }
238
+ if (this._isSoftDeleteEnabled) {
239
+ // SOFT DELETE: Update with flag and timestamp
240
+ yield (0, firestore_1.updateDoc)(docRef, {
241
+ isDeleted: true,
242
+ deletedAt: (0, firestore_1.serverTimestamp)(),
243
+ updatedAt: (0, firestore_1.serverTimestamp)()
244
+ });
245
+ }
246
+ else {
247
+ // HARD DELETE: Remove from disk
248
+ yield (0, firestore_1.deleteDoc)(docRef);
249
+ }
250
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
251
+ // collection are now potentially stale.
252
+ this._invalidateCache();
253
+ return { acknowledged: true, deletedCount: 1 };
254
+ });
255
+ /**
256
+ * FIRESTOREDB: deleteMany()
257
+ * Uses Batched Writes for speed.
258
+ */
259
+ this.deleteMany = (docIds) => __awaiter(this, void 0, void 0, function* () {
260
+ const CHUNK_SIZE = 500;
261
+ const batchPromises = [];
262
+ let totalDeleted = 0;
263
+ for (let i = 0; i < docIds.length; i += CHUNK_SIZE) {
264
+ const batch = (0, firestore_1.writeBatch)(this._db);
265
+ const chunk = docIds.slice(i, i + CHUNK_SIZE);
266
+ chunk.forEach(id => {
267
+ let docRef = (0, firestore_1.doc)(this._db, this._collectionRef.id, id);
268
+ if (this._isSoftDeleteEnabled) {
269
+ batch.update(docRef, {
270
+ isDeleted: true,
271
+ deletedAt: (0, firestore_1.serverTimestamp)(),
272
+ updatedAt: (0, firestore_1.serverTimestamp)()
273
+ });
274
+ }
275
+ else {
276
+ batch.delete(docRef);
277
+ }
278
+ totalDeleted++;
279
+ });
280
+ batchPromises.push(batch.commit());
281
+ }
282
+ yield Promise.all(batchPromises);
283
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
284
+ // collection are now potentially stale.
285
+ this._invalidateCache();
286
+ return { acknowledged: true, deletedCount: totalDeleted };
287
+ });
288
+ /**
289
+ * Efficiently checks if a document exists without downloading it. (only metadata).
290
+ * Optimized to only check for the presence of a document
291
+ */
292
+ this.exists = (filter) => __awaiter(this, void 0, void 0, function* () {
293
+ const q = (0, firestore_1.query)(this._collectionRef, ...this._buildConstraints(filter), (0, firestore_1.limit)(1));
294
+ const snapshot = yield (0, firestore_1.getDocs)(q);
295
+ return !snapshot.empty;
296
+ });
297
+ /**
298
+ * FIRESTOREDB STYLE: aggregate({ status: 'sold' }, { total: { $sum: 'price' }, avg: { $avg: 'price' } })
299
+ */
300
+ this.aggregate = (...args_1) => __awaiter(this, [...args_1], void 0, function* (filter = {}, aggregations) {
301
+ const constraints = this._buildConstraints(filter);
302
+ const q = (0, firestore_1.query)(this._collectionRef, ...constraints);
303
+ // 1. Map the request to Firestore's aggregation functions
304
+ const spec = {};
305
+ Object.entries(aggregations).forEach(([alias, op]) => {
306
+ if (op.$sum)
307
+ spec[alias] = (0, firestore_1.sum)(op.$sum);
308
+ if (op.$avg)
309
+ spec[alias] = (0, firestore_1.average)(op.$avg);
310
+ if (op.$count)
311
+ spec[alias] = (0, firestore_1.count)();
312
+ });
313
+ // 2. Execute on server (Calculates across millions of docs instantly)
314
+ const snapshot = yield (0, firestore_1.getAggregateFromServer)(q, spec);
315
+ return snapshot.data();
316
+ });
317
+ this._flattenFilter = (obj, prefix = '') => {
318
+ let constraints = [];
319
+ Object.entries(obj).forEach(([key, value]) => {
320
+ const path = prefix ? `${prefix}.${key}` : key;
321
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
322
+ // Check if this object is a KnymbusDB operator (e.g. { $gt: 20 })
323
+ const firstKey = Object.keys(value)[0];
324
+ if (firstKey.startsWith("$")) {
325
+ // get the symbol if not found then default to equality
326
+ const op = utils_1.$OperatorMap[firstKey] || '==';
327
+ constraints.push((0, firestore_1.where)(path, op, value[firstKey]));
328
+ }
329
+ else {
330
+ // It's a nested object, keep flattening
331
+ constraints = [...constraints, ...this._flattenFilter(value, path)];
332
+ }
333
+ }
334
+ else {
335
+ // It's a primitive value, use standard equality
336
+ constraints.push((0, firestore_1.where)(path, '==', value));
337
+ }
338
+ });
339
+ return constraints;
340
+ };
341
+ this._db = db;
342
+ this._collectionName = collectionName;
343
+ // Ensure Store is initialized or passed in
344
+ this._collectionRef = (0, firestore_1.collection)(this._db, this._collectionName);
345
+ this._isSoftDeleteEnabled = (_a = options.softDelete) !== null && _a !== void 0 ? _a : false;
346
+ }
347
+ /**
348
+ * FIRESTOREDB: db.collection.restore(id)
349
+ * Reverses a soft delete by flipping the flag and removing deletedAt.
350
+ */
351
+ restore(id) {
352
+ return __awaiter(this, void 0, void 0, function* () {
353
+ const docRef = (0, firestore_1.doc)(this._db, this._collectionName, id);
354
+ yield (0, firestore_1.updateDoc)(docRef, {
355
+ isDeleted: false,
356
+ deletedAt: null, // Clear the timestamp
357
+ updatedAt: (0, firestore_1.serverTimestamp)()
358
+ });
359
+ // 2. TRIGGER INVALIDATION: Any cached 'find' results for this
360
+ // collection are now potentially stale.
361
+ this._invalidateCache();
362
+ return { acknowledged: true, restoredCount: 1 };
363
+ });
364
+ }
365
+ /**
366
+ * MONGODB: db.collection.watch(filter)
367
+ * A real-time listener that bypasses the manual cache and
368
+ * pushes updates as they happen in the database.
369
+ */
370
+ watch(filter = {}, callback, onError) {
371
+ const constraints = this._buildConstraints(filter);
372
+ const q = (0, firestore_1.query)(this._collectionRef, ...constraints);
373
+ // Returns the unsubscribe function
374
+ return (0, firestore_1.onSnapshot)(q, (snapshot) => {
375
+ const docs = snapshot.docs.map(d => {
376
+ const raw = Object.assign({ _id: d.id }, d.data());
377
+ return (0, utils_1.hydrateDates)(raw);
378
+ });
379
+ callback(docs);
380
+ }, (err) => {
381
+ if (onError)
382
+ onError(err);
383
+ else
384
+ console.error("FirestoreDB::watch:Error: ", err);
385
+ });
386
+ }
387
+ // PRIVATE METHODS
388
+ _buildConstraints(filter) {
389
+ return [...this._flattenFilter(filter)];
390
+ }
391
+ /**
392
+ * In FirestoreDB class:
393
+ * Purges all cached queries for THIS collection to ensure data freshness.
394
+ */
395
+ _invalidateCache() {
396
+ const prefix = `${this._collectionName}_`;
397
+ // Convert keys to an array before iterating to avoid
398
+ // "Concurrent Modification" issues while deleting
399
+ const keys = Array.from(FirestoreQuery_1.FirestoreQuery._globalCache.keys());
400
+ keys.forEach(key => {
401
+ if (key.startsWith(prefix)) {
402
+ FirestoreQuery_1.FirestoreQuery._globalCache.delete(key);
403
+ }
404
+ });
405
+ }
406
+ }
407
+ exports.FirestoreDB = FirestoreDB;
@@ -0,0 +1,58 @@
1
+ import { QueryConstraint } from "firebase/firestore";
2
+ import { WithSystemFields } from "./types";
3
+ import { LRUCache } from './utils';
4
+ export type SortDescriptor = "asc" | "desc";
5
+ export interface PaginationMetadata {
6
+ totalRecords: number;
7
+ totalPages: number;
8
+ hasNext: boolean;
9
+ hasPrevious: boolean;
10
+ }
11
+ export declare class FirestoreQuery<T> {
12
+ private db;
13
+ private collectionName;
14
+ private collectionRef;
15
+ private filter;
16
+ private buildConstraints;
17
+ private countDocs;
18
+ static _globalCache: LRUCache;
19
+ private _useCache;
20
+ private _ttl;
21
+ private _deleteMode;
22
+ private _limit;
23
+ private _sort;
24
+ private _cursorId?;
25
+ private _cursorType;
26
+ constructor(db: any, collectionName: string, collectionRef: any, filter: Record<string, any>, buildConstraints: (f: any) => QueryConstraint[], countDocs: (f: any) => Promise<number>, isSoftDeleteEnabled: boolean);
27
+ /** Enables result caching for this query */
28
+ cache(ttlMs?: number): this;
29
+ sort(sortObj: Record<keyof T, SortDescriptor>): this;
30
+ limit(n: number): this;
31
+ /** Move to the next page */
32
+ after(id: string): this;
33
+ /** Move to the previous page */
34
+ before(id: string): this;
35
+ /**
36
+ * FirestoreDB: db.collection.find({}).withDeleted().execute()
37
+ * Allows viewing soft-deleted documents for this specific query.
38
+ */
39
+ withDeleted(): this;
40
+ onlyDeleted(): this;
41
+ /** Simple execution (Returns Array) */
42
+ execute(): Promise<WithSystemFields<T>[]>;
43
+ /** Paginated execution (Returns Data + Metadata) */
44
+ paginate(): Promise<{
45
+ data: WithSystemFields<T>[];
46
+ metadata: PaginationMetadata;
47
+ }>;
48
+ stream(): ReadableStream<T & {
49
+ _id: string;
50
+ }>;
51
+ private _prepareConstraints;
52
+ private _generateCacheKey;
53
+ /**
54
+ * Helper to ensure {a:1, b:2} and {b:2, a:1} result in the same JSON string
55
+ */
56
+ private _sortObjectKeys;
57
+ }
58
+ //# sourceMappingURL=FirestoreQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FirestoreQuery.d.ts","sourceRoot":"","sources":["../../src/FirestoreQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAC8E,eAAe,EAEnG,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAG3C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,CAAA;AAC3C,MAAM,WAAW,kBAAkB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,cAAc,CAAC,CAAC;IAerB,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,SAAS;IAjBrB,OAAc,YAAY,WAAkB;IAC5C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,IAAI,CAAiB;IAG7B,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,WAAW,CAAmC;gBAG1C,EAAE,EAAE,GAAG,EACP,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,GAAG,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,gBAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,eAAe,EAAE,EAC/C,SAAS,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,EAC9C,mBAAmB,EAAE,OAAO;IAKhC,4CAA4C;IAC5C,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM;IAMb,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IAK7C,KAAK,CAAC,CAAC,EAAE,MAAM;IAKtB,4BAA4B;IACrB,KAAK,CAAC,EAAE,EAAE,MAAM;IAMvB,gCAAgC;IACzB,MAAM,CAAC,EAAE,EAAE,MAAM;IAMxB;;;OAGG;IACH,WAAW;IAKX,WAAW;IAEX,uCAAuC;IAC1B,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;IAUtD,oDAAoD;IACvC,QAAQ,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;QAAC,QAAQ,EAAE,kBAAkB,CAAA;KAAE,CAAC;IAsCxF,MAAM,IAAI,cAAc,CAAC,CAAC,GAAG;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;YAoBtC,mBAAmB;IAsCjC,OAAO,CAAC,iBAAiB;IAczB;;GAED;IACC,OAAO,CAAC,eAAe;CAQ1B"}
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FirestoreQuery = void 0;
13
+ const firestore_1 = require("firebase/firestore");
14
+ const utils_1 = require("./utils");
15
+ const Hasher_1 = require("./utils/Hasher");
16
+ const utils_2 = require("./utils");
17
+ class FirestoreQuery {
18
+ constructor(db, collectionName, collectionRef, filter, buildConstraints, countDocs, isSoftDeleteEnabled) {
19
+ this.db = db;
20
+ this.collectionName = collectionName;
21
+ this.collectionRef = collectionRef;
22
+ this.filter = filter;
23
+ this.buildConstraints = buildConstraints;
24
+ this.countDocs = countDocs;
25
+ this._useCache = false;
26
+ this._ttl = 60000; // Default 1 minute
27
+ this._deleteMode = 'hide';
28
+ this._limit = 100;
29
+ this._sort = {};
30
+ this._cursorType = null;
31
+ this._deleteMode = isSoftDeleteEnabled ? 'hide' : 'include';
32
+ }
33
+ /** Enables result caching for this query */
34
+ cache(ttlMs) {
35
+ this._useCache = true;
36
+ if (ttlMs)
37
+ this._ttl = ttlMs;
38
+ return this;
39
+ }
40
+ sort(sortObj) {
41
+ this._sort = sortObj;
42
+ return this;
43
+ }
44
+ limit(n) {
45
+ this._limit = n;
46
+ return this;
47
+ }
48
+ /** Move to the next page */
49
+ after(id) {
50
+ this._cursorId = id;
51
+ this._cursorType = 'after';
52
+ return this;
53
+ }
54
+ /** Move to the previous page */
55
+ before(id) {
56
+ this._cursorId = id;
57
+ this._cursorType = 'before';
58
+ return this;
59
+ }
60
+ /**
61
+ * FirestoreDB: db.collection.find({}).withDeleted().execute()
62
+ * Allows viewing soft-deleted documents for this specific query.
63
+ */
64
+ withDeleted() {
65
+ this._deleteMode = 'include';
66
+ return this;
67
+ }
68
+ onlyDeleted() { this._deleteMode = 'only'; return this; }
69
+ /** Simple execution (Returns Array) */
70
+ execute() {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ let finalConstraints = yield this._prepareConstraints();
73
+ const q = (0, firestore_1.query)(this.collectionRef, ...finalConstraints);
74
+ const snapshot = yield (0, firestore_1.getDocs)(q);
75
+ return snapshot.docs.map(d => {
76
+ const rawData = Object.assign({ _id: d.id }, (d.data() || {}));
77
+ return (0, utils_1.hydrateDates)(rawData);
78
+ });
79
+ });
80
+ }
81
+ /** Paginated execution (Returns Data + Metadata) */
82
+ paginate() {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ // set the global key for this query
85
+ const key = this._generateCacheKey();
86
+ // 1. Check Cache
87
+ if (this._useCache) {
88
+ const cached = FirestoreQuery._globalCache.get(key);
89
+ if (cached && (Date.now() - cached.timestamp < this._ttl)) {
90
+ return cached.data;
91
+ }
92
+ }
93
+ const data = yield this.execute();
94
+ const totalRecords = yield this.countDocs(this.filter);
95
+ const totalPages = this._limit > 0 ? Math.ceil(totalRecords / this._limit) : 1;
96
+ const result = {
97
+ data,
98
+ metadata: {
99
+ totalRecords,
100
+ totalPages,
101
+ hasNext: data.length === this._limit,
102
+ hasPrevious: !!this._cursorId
103
+ }
104
+ };
105
+ // 2. Fetch & Store
106
+ if (this._useCache) {
107
+ FirestoreQuery._globalCache.set(key, {
108
+ data: result,
109
+ timestamp: Date.now()
110
+ });
111
+ }
112
+ return result;
113
+ });
114
+ }
115
+ stream() {
116
+ const fetchDocs = () => __awaiter(this, void 0, void 0, function* () { return this.execute(); });
117
+ return new ReadableStream({
118
+ start(controller) {
119
+ return __awaiter(this, void 0, void 0, function* () {
120
+ try {
121
+ const docs = yield fetchDocs();
122
+ for (let d of docs) {
123
+ controller.enqueue(d); // push docs into pipe
124
+ }
125
+ controller.close(); // End of stream
126
+ }
127
+ catch (error) {
128
+ controller.error(error);
129
+ }
130
+ });
131
+ }
132
+ });
133
+ }
134
+ // PRIVATE METHODS
135
+ _prepareConstraints() {
136
+ return __awaiter(this, void 0, void 0, function* () {
137
+ const constraints = [...this.buildConstraints(this.filter)];
138
+ // If mode is 'include', we simply don't add an isDeleted constraint at all
139
+ if (this._deleteMode === 'hide') {
140
+ constraints.push((0, firestore_1.where)("isDeleted", "==", false));
141
+ }
142
+ else if (this._deleteMode === 'only') {
143
+ constraints.push((0, firestore_1.where)("isDeleted", "==", true));
144
+ }
145
+ // 1. Apply Sorting (Required for cursors)
146
+ Object.entries(this._sort).forEach(([field, dir]) => {
147
+ constraints.push((0, firestore_1.orderBy)(field, dir));
148
+ });
149
+ // 2. Handle Cursors (Validation + Logic)
150
+ if (this._cursorId) {
151
+ const docRef = (0, firestore_1.doc)(this.db, this.collectionName, this._cursorId);
152
+ const snap = yield (0, firestore_1.getDoc)(docRef);
153
+ if (snap.exists()) {
154
+ if (this._cursorType === 'after') {
155
+ constraints.push((0, firestore_1.startAfter)(snap));
156
+ if (this._limit)
157
+ constraints.push((0, firestore_1.limit)(this._limit));
158
+ }
159
+ // If not start then before
160
+ else {
161
+ constraints.push((0, firestore_1.endBefore)(snap));
162
+ // limitToLast is required to get the 10 items *closest* to the cursor
163
+ if (this._limit)
164
+ constraints.push((0, firestore_1.limitToLast)(this._limit));
165
+ }
166
+ }
167
+ }
168
+ else if (this._limit) {
169
+ constraints.push((0, firestore_1.limit)(this._limit));
170
+ }
171
+ return constraints;
172
+ });
173
+ }
174
+ _generateCacheKey() {
175
+ const params = {
176
+ filter: this._sortObjectKeys(this.filter), // Sort keys for consistency
177
+ sort: this._sort,
178
+ limit: this._limit,
179
+ cursorId: this._cursorId,
180
+ cursorType: this._cursorType,
181
+ includeDeleted: this._deleteMode
182
+ };
183
+ // Use the static Hasher class
184
+ return Hasher_1.Hasher.generateCacheKey(this.collectionName, params);
185
+ }
186
+ /**
187
+ * Helper to ensure {a:1, b:2} and {b:2, a:1} result in the same JSON string
188
+ */
189
+ _sortObjectKeys(obj) {
190
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj))
191
+ return obj;
192
+ return Object.keys(obj).sort().reduce((acc, key) => {
193
+ acc[key] = this._sortObjectKeys(obj[key]);
194
+ return acc;
195
+ }, {});
196
+ }
197
+ }
198
+ exports.FirestoreQuery = FirestoreQuery;
199
+ // CACHE VARS
200
+ FirestoreQuery._globalCache = new utils_2.LRUCache(); // Shared across instances