flongo 1.0.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.
@@ -0,0 +1,400 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FlongoCollection = void 0;
4
+ const flongo_1 = require("./flongo");
5
+ const flongoQuery_1 = require("./flongoQuery");
6
+ const types_1 = require("./types");
7
+ const mongodb_1 = require("mongodb");
8
+ const errors_1 = require("./errors");
9
+ /**
10
+ * FlongoCollection provides a high-level interface for MongoDB collection operations
11
+ * with automatic entity management, event logging, and fluent query support.
12
+ *
13
+ * This class abstracts MongoDB collection operations and provides:
14
+ * - Automatic _id conversion between ObjectId and string
15
+ * - Timestamps management (createdAt, updatedAt)
16
+ * - Optional event logging for audit trails
17
+ * - Integration with FlongoQuery for fluent querying
18
+ * - Type-safe operations with TypeScript generics
19
+ *
20
+ * Example usage:
21
+ * ```typescript
22
+ * interface User {
23
+ * name: string;
24
+ * email: string;
25
+ * age: number;
26
+ * }
27
+ *
28
+ * const users = new FlongoCollection<User>('users');
29
+ *
30
+ * // Create a user
31
+ * const user = await users.create({ name: 'John', email: 'john@example.com', age: 30 });
32
+ *
33
+ * // Query users
34
+ * const adults = await users.getAll(
35
+ * new FlongoQuery().where('age').gtEq(18)
36
+ * );
37
+ * ```
38
+ */
39
+ class FlongoCollection {
40
+ /**
41
+ * Creates a new FlongoCollection instance
42
+ * @param collectionName - Name of the MongoDB collection
43
+ * @param options - Configuration options for the collection
44
+ */
45
+ constructor(collectionName, options = {}) {
46
+ this.collection = flongo_1.flongoDb.collection(collectionName);
47
+ this.options = { enableEventLogging: true, eventsCollectionName: "events", ...options };
48
+ // Initialize events collection only if logging is enabled
49
+ this.events = this.options.enableEventLogging
50
+ ? flongo_1.flongoDb.collection(this.options.eventsCollectionName)
51
+ : null;
52
+ this.name = collectionName;
53
+ }
54
+ // ===========================================
55
+ // READ OPERATIONS
56
+ // ===========================================
57
+ /**
58
+ * Retrieves a single document by its ID
59
+ * @param id - Document ID (string that will be converted to ObjectId)
60
+ * @returns Promise resolving to the document with Entity metadata
61
+ * @throws Error404 if document is not found
62
+ */
63
+ async get(id) {
64
+ const objId = new mongodb_1.ObjectId(id);
65
+ const docWithId = await this.collection.findOne({ _id: objId });
66
+ if (docWithId) {
67
+ return this.toEntity(docWithId);
68
+ }
69
+ else {
70
+ console.error("Could not find id: " + id);
71
+ throw new errors_1.Error404();
72
+ }
73
+ }
74
+ /**
75
+ * Converts MongoDB document to Entity format
76
+ * Converts ObjectId _id to string and ensures proper typing
77
+ * @private
78
+ * @param obj - Raw MongoDB document
79
+ * @returns Document with string _id and Entity typing
80
+ */
81
+ toEntity(obj) {
82
+ return { ...obj, _id: String(obj._id) };
83
+ }
84
+ /**
85
+ * Retrieves multiple documents based on query and pagination
86
+ * @param query - Optional FlongoQuery for filtering
87
+ * @param pagination - Optional pagination settings
88
+ * @returns Promise resolving to array of documents
89
+ */
90
+ async getAll(query, pagination) {
91
+ const mongodbQuery = query?.build() ?? {};
92
+ const mongodbOptions = query?.buildOptions(pagination) ?? (new flongoQuery_1.FlongoQuery()).buildOptions(pagination);
93
+ try {
94
+ const res = await this.collection.find(mongodbQuery, mongodbOptions).toArray();
95
+ return res.map((d) => this.toEntity(d));
96
+ }
97
+ catch (err) {
98
+ throw err;
99
+ }
100
+ }
101
+ /**
102
+ * Retrieves a subset of documents with required query and pagination
103
+ * Similar to getAll but requires both query and pagination parameters
104
+ * @param query - FlongoQuery for filtering (required)
105
+ * @param pagination - Pagination settings (required)
106
+ * @returns Promise resolving to array of documents
107
+ */
108
+ async getSome(query, pagination) {
109
+ const mongodbQuery = query?.build();
110
+ const mongodbOptions = query?.buildOptions(pagination);
111
+ return (await this.collection.find(mongodbQuery, mongodbOptions).toArray()).map((d) => this.toEntity(d));
112
+ }
113
+ /**
114
+ * Retrieves the first document matching the query
115
+ * @param query - FlongoQuery for filtering
116
+ * @returns Promise resolving to the first matching document
117
+ */
118
+ async getFirst(query) {
119
+ const mongodbQuery = query?.build();
120
+ const mongodbOptions = query?.buildOptions();
121
+ return this.toEntity(await this.collection.findOne(mongodbQuery, mongodbOptions));
122
+ }
123
+ /**
124
+ * Counts documents matching the query
125
+ * @param query - Optional FlongoQuery for filtering
126
+ * @returns Promise resolving to the count of matching documents
127
+ */
128
+ async count(query) {
129
+ const mongodbQuery = query?.build() ?? {};
130
+ return this.collection.countDocuments(mongodbQuery);
131
+ }
132
+ /**
133
+ * Checks if any documents match the query
134
+ * @param query - FlongoQuery for filtering
135
+ * @returns Promise resolving to true if any documents match, false otherwise
136
+ */
137
+ async exists(query) {
138
+ try {
139
+ const count = await this.collection.countDocuments(query.build());
140
+ return count > 0;
141
+ }
142
+ catch (err) {
143
+ return false;
144
+ }
145
+ }
146
+ // ===========================================
147
+ // DELETE OPERATIONS
148
+ // ===========================================
149
+ /**
150
+ * Deletes a single document by ID
151
+ * Logs the deletion event with backup data before deleting
152
+ * @param id - Document ID to delete
153
+ * @param clientId - ID of the client performing the deletion (for audit trail)
154
+ */
155
+ async delete(id, clientId) {
156
+ // Get the entity before deletion for backup logging
157
+ const entity = await this.get(id);
158
+ // Log deletion event with backup data
159
+ this.logEvent({
160
+ name: types_1.EventName.DeleteEntity,
161
+ value: {
162
+ id,
163
+ collectionType: this.name,
164
+ count: await this.count(),
165
+ backup: entity,
166
+ deletedBy: clientId
167
+ }
168
+ });
169
+ // Perform the actual deletion
170
+ await this.collection.deleteOne({ _id: new mongodb_1.ObjectId(id) });
171
+ }
172
+ /**
173
+ * Deletes multiple documents by their IDs
174
+ * Logs batch deletion event with backup data before deleting
175
+ * @param ids - Array of document IDs to delete
176
+ * @param clientId - ID of the client performing the deletion
177
+ */
178
+ async batchDelete(ids, clientId) {
179
+ // Note: There's a bug in the original code - mongodbQuery is incorrectly structured
180
+ // It should use ObjectIds, but we'll preserve the original logic for now
181
+ const mongodbQuery = { _id: { $in: ids } };
182
+ // Get entities before deletion for backup logging
183
+ const entities = await this.getAll(new flongoQuery_1.FlongoQuery().where("_id").in(ids));
184
+ // Log batch deletion event
185
+ this.logEvent({
186
+ name: types_1.EventName.BatchDeleteEntities,
187
+ value: {
188
+ collectionType: this.name,
189
+ count: await this.count(),
190
+ backup: entities,
191
+ deletedBy: clientId
192
+ }
193
+ });
194
+ // Perform the actual deletion
195
+ // Note: This should be corrected to use proper MongoDB query structure
196
+ await this.collection.deleteMany({ mongodbQuery });
197
+ }
198
+ // ===========================================
199
+ // CREATE OPERATIONS
200
+ // ===========================================
201
+ /**
202
+ * Creates a new document in the collection
203
+ * Automatically adds timestamps and logs the creation event
204
+ * @param attributes - Document data (without Entity metadata)
205
+ * @param clientId - Optional client ID for audit trail
206
+ * @returns Promise resolving to the created document with Entity metadata
207
+ */
208
+ async create(attributes, clientId) {
209
+ try {
210
+ // Insert document with automatic timestamps
211
+ const created = await this.collection.insertOne({
212
+ ...attributes,
213
+ createdAt: Date.now(),
214
+ updatedAt: Date.now()
215
+ });
216
+ // Retrieve the created document to get the full entity
217
+ const entity = this.toEntity(await this.get(created.insertedId));
218
+ // Log creation event
219
+ this.logEvent({
220
+ name: types_1.EventName.CreateEntity,
221
+ identity: clientId,
222
+ value: { id: entity._id, collectionType: this.name, count: await this.count() }
223
+ });
224
+ return entity;
225
+ }
226
+ catch (err) {
227
+ console.error("Failed create: ", err);
228
+ throw err;
229
+ }
230
+ }
231
+ /**
232
+ * Creates multiple documents in a single batch operation
233
+ * More efficient than individual creates for large datasets
234
+ * @param attributes - Array of document data
235
+ * @param clientId - Optional client ID for audit trail
236
+ */
237
+ async batchCreate(attributes, clientId) {
238
+ if (attributes.length === 0)
239
+ return;
240
+ const now = Date.now();
241
+ // Prepare entities with timestamps
242
+ const entities = attributes.map((attribute) => ({
243
+ ...attribute,
244
+ createdAt: now,
245
+ updatedAt: now
246
+ }));
247
+ // Insert all entities at once
248
+ await this.collection.insertMany(entities);
249
+ // Log batch creation event
250
+ this.logEvent({
251
+ name: types_1.EventName.BatchCreateEntities,
252
+ identity: clientId,
253
+ value: { collectionType: this.name, count: await this.count() }
254
+ });
255
+ }
256
+ // ===========================================
257
+ // UPDATE OPERATIONS
258
+ // ===========================================
259
+ /**
260
+ * Updates multiple documents matching the query
261
+ * @param attributes - Fields to update
262
+ * @param query - Optional FlongoQuery to filter documents (updates all if not provided)
263
+ * @param clientId - Optional client ID for audit trail
264
+ */
265
+ async updateAll(attributes, query, clientId) {
266
+ const mongodbQuery = query?.build() ?? {};
267
+ // Perform the update with automatic updatedAt timestamp
268
+ await this.collection.updateMany(mongodbQuery, {
269
+ $set: {
270
+ updatedAt: Date.now(),
271
+ ...attributes
272
+ }
273
+ });
274
+ // Log batch update event
275
+ this.logEvent({
276
+ name: types_1.EventName.BatchUpdateEntities,
277
+ identity: clientId,
278
+ value: { collectionType: this.name }
279
+ });
280
+ }
281
+ /**
282
+ * Updates a single document by ID
283
+ * @param id - Document ID to update
284
+ * @param attributes - Fields to update
285
+ * @param clientId - Optional client ID for audit trail
286
+ */
287
+ async update(id, attributes, clientId) {
288
+ // Prevent _id from being updated (safety measure)
289
+ if ("_id" in attributes) {
290
+ delete attributes._id;
291
+ }
292
+ try {
293
+ // Perform the update with automatic updatedAt timestamp
294
+ await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id) }, {
295
+ $set: {
296
+ updatedAt: Date.now(),
297
+ ...attributes
298
+ }
299
+ });
300
+ // Log update event
301
+ this.logEvent({
302
+ name: types_1.EventName.UpdateEntity,
303
+ identity: clientId,
304
+ value: { collectionType: this.name, id }
305
+ });
306
+ }
307
+ catch (err) {
308
+ console.error("Failed updating: ", err);
309
+ throw err;
310
+ }
311
+ }
312
+ /**
313
+ * Updates the first document matching the query
314
+ * @param attributes - Fields to update
315
+ * @param query - Optional FlongoQuery to filter documents
316
+ * @param clientId - Optional client ID for audit trail
317
+ * @returns Promise resolving to the updated document
318
+ */
319
+ async updateFirst(attributes, query, clientId) {
320
+ const mongodbQuery = query?.build() ?? {};
321
+ const update = {
322
+ $set: {
323
+ updatedAt: Date.now(),
324
+ ...attributes
325
+ }
326
+ };
327
+ const updated = await this.collection.findOneAndUpdate(mongodbQuery, update);
328
+ if (updated) {
329
+ return this.toEntity(updated);
330
+ }
331
+ else {
332
+ throw new Error("Failed to update entity");
333
+ }
334
+ }
335
+ // ===========================================
336
+ // ATOMIC OPERATIONS
337
+ // ===========================================
338
+ /**
339
+ * Atomically increments a numeric field
340
+ * @param id - Document ID
341
+ * @param key - Field name to increment
342
+ * @param amt - Amount to increment by (defaults to 1)
343
+ */
344
+ async increment(id, key, amt) {
345
+ const inc = { $inc: { [key]: amt || 1 } };
346
+ await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id) }, inc);
347
+ }
348
+ /**
349
+ * Atomically decrements a numeric field
350
+ * @param id - Document ID
351
+ * @param key - Field name to decrement
352
+ * @param amt - Amount to decrement by (defaults to 1)
353
+ */
354
+ async decrement(id, key, amt) {
355
+ await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id) }, { $inc: { [key]: -(amt ?? 1) } });
356
+ }
357
+ /**
358
+ * Atomically appends items to an array field
359
+ * @param id - Document ID
360
+ * @param key - Array field name
361
+ * @param items - Items to append to the array
362
+ */
363
+ async append(id, key, items) {
364
+ await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id) }, {
365
+ $push: { [key]: { $each: items } }
366
+ });
367
+ }
368
+ /**
369
+ * Atomically removes items from an array field
370
+ * @param id - Document ID
371
+ * @param key - Array field name
372
+ * @param items - Items to remove from the array
373
+ */
374
+ async arrRemove(id, key, items) {
375
+ await this.collection.updateOne({ _id: new mongodb_1.ObjectId(id) }, { $pull: { [key]: { $in: items } } });
376
+ }
377
+ // ===========================================
378
+ // EVENT LOGGING
379
+ // ===========================================
380
+ /**
381
+ * Logs an event to the events collection for audit trails
382
+ * Events are only logged if event logging is enabled in options
383
+ * @param event - Event object with name and metadata
384
+ * @returns Promise resolving to the event ID (or null if logging disabled)
385
+ */
386
+ async logEvent(event) {
387
+ if (!this.events) {
388
+ return null; // Event logging is disabled
389
+ }
390
+ // Insert event with automatic timestamps
391
+ const created = await this.events.insertOne({
392
+ ...event,
393
+ createdAt: Date.now(),
394
+ updatedAt: Date.now()
395
+ });
396
+ return String(created.insertedId);
397
+ }
398
+ }
399
+ exports.FlongoCollection = FlongoCollection;
400
+ //# sourceMappingURL=flongoCollection.js.map
@@ -0,0 +1,230 @@
1
+ import { Bounds, Coordinates, Pagination, ColExpression, ColRange, ICollectionQuery, Logic, SortDirection } from "./types";
2
+ import { Document, Filter, FindOptions } from "mongodb";
3
+ /**
4
+ * FlongoQuery provides a fluent, chainable interface for building MongoDB queries
5
+ * similar to Firestore's query API. It allows developers to construct complex queries
6
+ * using method chaining rather than building MongoDB filter objects manually.
7
+ *
8
+ * Example usage:
9
+ * ```typescript
10
+ * const query = new FlongoQuery()
11
+ * .where('age').gtEq(18)
12
+ * .and('status').eq('active')
13
+ * .and('tags').arrContainsAny(['developer', 'designer'])
14
+ * .orderBy('createdAt', SortDirection.Descending);
15
+ * ```
16
+ */
17
+ export declare class FlongoQuery implements ICollectionQuery {
18
+ /** Array of field expressions (where conditions) */
19
+ expressions: ColExpression[];
20
+ /** Array of range queries (currently unused but reserved for future features) */
21
+ ranges: ColRange[];
22
+ /** Field to sort results by */
23
+ orderField?: string;
24
+ /** Direction for sorting (ascending or descending) */
25
+ orderDirection?: SortDirection;
26
+ /** Array of queries to be combined with OR logic */
27
+ orQueries: FlongoQuery[];
28
+ /** Array of queries to be combined with AND logic */
29
+ andQueries: FlongoQuery[];
30
+ /**
31
+ * Gets the most recently added expression for method chaining
32
+ * @private
33
+ * @returns The last expression in the expressions array
34
+ */
35
+ private exp;
36
+ /**
37
+ * Sets the operator and value for the most recent expression
38
+ * If value is null or undefined, removes the last expression (allows for conditional chaining)
39
+ * @private
40
+ * @param op - MongoDB operator (e.g., '$eq', '$gt', '$in')
41
+ * @param val - Value to compare against
42
+ * @returns This query instance for chaining
43
+ */
44
+ set(op?: string, val?: any): FlongoQuery;
45
+ /**
46
+ * Helper method to handle empty/invalid values by removing the last expression
47
+ * @private
48
+ * @returns This query instance for chaining
49
+ */
50
+ private handleEmptyValue;
51
+ /**
52
+ * Sets a range query on a field (currently unused but reserved for future features)
53
+ * @param key - Field name to query
54
+ * @param start - Start value of the range
55
+ * @param end - End value of the range
56
+ * @param orderField - Field to order by for range queries
57
+ */
58
+ setRange(key: string, start: any, end: any, orderField: string): void;
59
+ /**
60
+ * Starts a new where clause for the specified field
61
+ * @param key - Field name to query
62
+ * @returns This query instance for chaining
63
+ */
64
+ where(key: string): FlongoQuery;
65
+ /**
66
+ * Alias for where() - adds another field constraint with AND logic
67
+ * @param key - Field name to query
68
+ * @returns This query instance for chaining
69
+ */
70
+ and(key: string): FlongoQuery;
71
+ /**
72
+ * Adds equality constraint to the current field
73
+ * @param val - Value to match exactly
74
+ * @returns This query instance for chaining
75
+ */
76
+ eq(val?: any): FlongoQuery;
77
+ /**
78
+ * Adds not-equal constraint to the current field
79
+ * @param val - Value to exclude
80
+ * @returns This query instance for chaining
81
+ */
82
+ neq(val?: any): FlongoQuery;
83
+ /**
84
+ * Adds less-than constraint to the current field
85
+ * @param val - Upper bound (exclusive)
86
+ * @returns This query instance for chaining
87
+ */
88
+ lt(val?: any): FlongoQuery;
89
+ /**
90
+ * Adds less-than-or-equal constraint to the current field
91
+ * @param val - Upper bound (inclusive)
92
+ * @returns This query instance for chaining
93
+ */
94
+ ltEq(val?: any): FlongoQuery;
95
+ /**
96
+ * Adds greater-than constraint to the current field
97
+ * @param val - Lower bound (exclusive)
98
+ * @returns This query instance for chaining
99
+ */
100
+ gt(val?: any): FlongoQuery;
101
+ /**
102
+ * Adds greater-than-or-equal constraint to the current field
103
+ * @param val - Lower bound (inclusive)
104
+ * @returns This query instance for chaining
105
+ */
106
+ gtEq(val?: any): FlongoQuery;
107
+ /**
108
+ * Checks if array field contains the specified value
109
+ * @param val - Value that must be present in the array
110
+ * @returns This query instance for chaining
111
+ */
112
+ arrContains(val?: any): FlongoQuery;
113
+ /**
114
+ * Checks if array field contains all of the specified values
115
+ * @param val - Array of values that must all be present
116
+ * @returns This query instance for chaining
117
+ */
118
+ arrContainsAll(val?: any): FlongoQuery;
119
+ /**
120
+ * Checks if array field contains any of the specified values
121
+ * If empty array is provided, removes the expression
122
+ * @param val - Array of values, any of which may be present
123
+ * @returns This query instance for chaining
124
+ */
125
+ arrContainsAny(val?: any[]): FlongoQuery;
126
+ /**
127
+ * Adds case-insensitive string starts-with constraint
128
+ * @param val - String prefix to match
129
+ * @returns This query instance for chaining
130
+ */
131
+ startsWith(val: string): FlongoQuery;
132
+ /**
133
+ * Adds case-insensitive string ends-with constraint
134
+ * @param val - String suffix to match
135
+ * @returns This query instance for chaining
136
+ */
137
+ endsWith(val: string): FlongoQuery;
138
+ /**
139
+ * Adds case-insensitive string contains constraint
140
+ * @param val - Substring to search for
141
+ * @returns This query instance for chaining
142
+ */
143
+ strContains(val: string): FlongoQuery;
144
+ /**
145
+ * Helper method to apply comparison operators with null/empty checks
146
+ * @private
147
+ * @param op - MongoDB operator
148
+ * @param val - Array of values
149
+ * @returns This query instance for chaining
150
+ */
151
+ private applyComparisonOperator;
152
+ /**
153
+ * Checks if field value is in the provided array
154
+ * @param val - Array of possible values
155
+ * @returns This query instance for chaining
156
+ */
157
+ in(val?: any[]): FlongoQuery;
158
+ /**
159
+ * Checks if field value is NOT in the provided array
160
+ * @param val - Array of values to exclude
161
+ * @returns This query instance for chaining
162
+ */
163
+ notIn(val?: any[]): FlongoQuery;
164
+ /**
165
+ * Convenience method for range queries (field >= min AND field <= max)
166
+ * @param key - Field name
167
+ * @param min - Minimum value (inclusive)
168
+ * @param max - Maximum value (inclusive)
169
+ * @returns This query instance for chaining
170
+ */
171
+ inRange(key: string, min: any, max: any): FlongoQuery;
172
+ /**
173
+ * Performs geospatial radius search using geohash bounds
174
+ * This method uses the geofire-common library to generate geohash query bounds
175
+ * and creates OR queries for each bound to efficiently search within a radius
176
+ * @param key - Field containing geohash data
177
+ * @param center - Center point coordinates
178
+ * @param radius - Search radius in meters
179
+ * @returns This query instance for chaining
180
+ */
181
+ inRadius(key: string, center: Coordinates, radius: number): FlongoQuery;
182
+ /**
183
+ * Performs geospatial query within rectangular bounds
184
+ * Creates a GeoJSON polygon from the provided bounds
185
+ * @param bounds - Rectangular bounds with northeast and southwest corners
186
+ * @returns This query instance for chaining
187
+ */
188
+ geoWithin(bounds?: Bounds): FlongoQuery;
189
+ /**
190
+ * Sets the field and direction for sorting results
191
+ * @param field - Field name to sort by
192
+ * @param direction - Sort direction (ascending or descending)
193
+ * @returns This query instance for chaining
194
+ */
195
+ orderBy(field?: string, direction?: SortDirection): FlongoQuery;
196
+ /**
197
+ * Adds a sub-query with AND logic
198
+ * @param query - Sub-query to combine with AND logic
199
+ * @returns This query instance for chaining
200
+ */
201
+ andQuery(query: FlongoQuery): FlongoQuery;
202
+ /**
203
+ * Adds a sub-query with OR logic
204
+ * @param query - Sub-query to combine with OR logic
205
+ * @returns This query instance for chaining
206
+ */
207
+ or(query: FlongoQuery): FlongoQuery;
208
+ /**
209
+ * Builds the final MongoDB filter object from all expressions and sub-queries
210
+ * This method converts the fluent query structure into a MongoDB-compatible filter
211
+ * @returns MongoDB filter object
212
+ */
213
+ build<T>(): Filter<T>;
214
+ /**
215
+ * Builds MongoDB FindOptions for pagination and sorting
216
+ * @param pagination - Optional pagination settings
217
+ * @returns MongoDB FindOptions object
218
+ */
219
+ buildOptions<T extends Document>(pagination?: Pagination): FindOptions<T>;
220
+ }
221
+ /**
222
+ * FlongoQueryBuilder provides a builder pattern wrapper around FlongoQuery
223
+ * This can be useful for more complex query construction scenarios
224
+ */
225
+ export declare class FlongoQueryBuilder {
226
+ /** The underlying query instance */
227
+ q: FlongoQuery;
228
+ }
229
+ export { Logic };
230
+ //# sourceMappingURL=flongoQuery.d.ts.map