fix(cursor): Improve cursor usage documentation and refactor getCursor API to support native cursor modifiers
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-04-24 - 5.15.1 - fix(cursor) | ||||
| Improve cursor usage documentation and refactor getCursor API to support native cursor modifiers | ||||
|  | ||||
| - Updated examples in readme.md to demonstrate manual iteration using cursor.next() and proper cursor closing. | ||||
| - Refactored the getCursor method in classes.doc.ts to accept session and modifier options, consolidating cursor handling. | ||||
| - Added new tests in test/test.cursor.ts to verify cursor operations, including limits, sorting, and skipping. | ||||
|  | ||||
| ## 2025-04-24 - 5.15.0 - feat(svDb) | ||||
| Enhance svDb decorator to support custom serialization and deserialization options | ||||
|  | ||||
|   | ||||
							
								
								
									
										39
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								readme.md
									
									
									
									
									
								
							| @@ -133,31 +133,34 @@ const user = await User.getInstance({ username: 'myUsername' }); | ||||
| // Fetch multiple users that match criteria | ||||
| const users = await User.getInstances({ email: 'myEmail@example.com' }); | ||||
|  | ||||
| // Using a cursor for large collections | ||||
| // Obtain a cursor for large result sets | ||||
| const cursor = await User.getCursor({ active: true }); | ||||
|  | ||||
| // Process documents one at a time (memory efficient) | ||||
| await cursor.forEach(async (user, index) => { | ||||
|   // Process each user with its position | ||||
|   console.log(`Processing user ${index}: ${user.username}`); | ||||
| // Stream each document efficiently | ||||
| await cursor.forEach(async (user) => { | ||||
|   console.log(`Processing user: ${user.username}`); | ||||
| }); | ||||
|  | ||||
| // Chain cursor methods like in the MongoDB native driver | ||||
| const paginatedCursor = await User.getCursor({ active: true }) | ||||
|   .limit(10) // Limit results | ||||
|   .skip(20) // Skip first 20 results | ||||
|   .sort({ createdAt: -1 }); // Sort by creation date descending | ||||
| // Manually iterate using next() | ||||
| let nextUser; | ||||
| while ((nextUser = await cursor.next())) { | ||||
|   console.log(`Next user: ${nextUser.username}`); | ||||
| } | ||||
|  | ||||
| // Convert cursor to array (when you know the result set is small) | ||||
| const userArray = await paginatedCursor.toArray(); | ||||
| // Convert to array when the result set is small | ||||
| const userArray = await cursor.toArray(); | ||||
|  | ||||
| // Other cursor operations | ||||
| const nextUser = await cursor.next(); // Get the next document | ||||
| const hasMoreUsers = await cursor.hasNext(); // Check if more documents exist | ||||
| const count = await cursor.count(); // Get the count of documents in the cursor | ||||
|  | ||||
| // Always close cursors when done with them | ||||
| // Close the cursor to free resources | ||||
| await cursor.close(); | ||||
|  | ||||
| // For native cursor modifiers (sort, skip, limit), use getCursor with modifier option: | ||||
| const paginatedCursor = await User.getCursor( | ||||
|   { active: true }, | ||||
|   { modifier: (c) => c.sort({ createdAt: -1 }).skip(20).limit(10) } | ||||
| ); | ||||
| await paginatedCursor.forEach((user) => { | ||||
|   console.log(`Paginated user: ${user.username}`); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| #### Update | ||||
|   | ||||
							
								
								
									
										97
									
								
								test/test.cursor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test/test.cursor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import * as smartmongo from '@push.rocks/smartmongo'; | ||||
| import { smartunique } from '../ts/plugins.js'; | ||||
| import * as smartdata from '../ts/index.js'; | ||||
|  | ||||
| // Set up database connection | ||||
| let smartmongoInstance: smartmongo.SmartMongo; | ||||
| let testDb: smartdata.SmartdataDb; | ||||
|  | ||||
| // Define a simple document model for cursor tests | ||||
| @smartdata.Collection(() => testDb) | ||||
| class CursorTest extends smartdata.SmartDataDbDoc<CursorTest, CursorTest> { | ||||
|   @smartdata.unI() | ||||
|   public id: string = smartunique.shortId(); | ||||
|  | ||||
|   @smartdata.svDb() | ||||
|   public name: string; | ||||
|  | ||||
|   @smartdata.svDb() | ||||
|   public order: number; | ||||
|  | ||||
|   constructor(name: string, order: number) { | ||||
|     super(); | ||||
|     this.name = name; | ||||
|     this.order = order; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Initialize the in-memory MongoDB and SmartdataDB | ||||
| tap.test('cursor init: start Mongo and SmartdataDb', async () => { | ||||
|   smartmongoInstance = await smartmongo.SmartMongo.createAndStart(); | ||||
|   testDb = new smartdata.SmartdataDb( | ||||
|     await smartmongoInstance.getMongoDescriptor(), | ||||
|   ); | ||||
|   await testDb.init(); | ||||
| }); | ||||
|  | ||||
| // Insert sample documents | ||||
| tap.test('cursor insert: save 5 test documents', async () => { | ||||
|   for (let i = 1; i <= 5; i++) { | ||||
|     const doc = new CursorTest(`item${i}`, i); | ||||
|     await doc.save(); | ||||
|   } | ||||
|   const count = await CursorTest.getCount({}); | ||||
|   expect(count).toEqual(5); | ||||
| }); | ||||
|  | ||||
| // Test that toArray returns all documents | ||||
| tap.test('cursor toArray: retrieves all documents', async () => { | ||||
|   const cursor = await CursorTest.getCursor({}); | ||||
|   const all = await cursor.toArray(); | ||||
|   expect(all.length).toEqual(5); | ||||
| }); | ||||
|  | ||||
| // Test iteration via forEach | ||||
| tap.test('cursor forEach: iterates through all documents', async () => { | ||||
|   const names: string[] = []; | ||||
|   const cursor = await CursorTest.getCursor({}); | ||||
|   await cursor.forEach(async (item) => { | ||||
|     names.push(item.name); | ||||
|   }); | ||||
|   expect(names.length).toEqual(5); | ||||
|   expect(names).toContain('item3'); | ||||
| }); | ||||
|  | ||||
| // Test native cursor modifiers: limit | ||||
| tap.test('cursor modifier limit: only two documents', async () => { | ||||
|   const cursor = await CursorTest.getCursor({}, { modifier: (c) => c.limit(2) }); | ||||
|   const limited = await cursor.toArray(); | ||||
|   expect(limited.length).toEqual(2); | ||||
| }); | ||||
|  | ||||
| // Test native cursor modifiers: sort and skip | ||||
| tap.test('cursor modifier sort & skip: returns correct order', async () => { | ||||
|   const cursor = await CursorTest.getCursor({}, { | ||||
|     modifier: (c) => c.sort({ order: -1 }).skip(1), | ||||
|   }); | ||||
|   const results = await cursor.toArray(); | ||||
|   // Skipped the first (order 5), next should be 4,3,2,1 | ||||
|   expect(results.length).toEqual(4); | ||||
|   expect(results[0].order).toEqual(4); | ||||
| }); | ||||
|  | ||||
| // Cleanup: drop database, close connections, stop Mongo | ||||
| tap.test('cursor cleanup: drop DB and stop', async () => { | ||||
|   await testDb.mongoDb.dropDatabase(); | ||||
|   await testDb.close(); | ||||
|   if (smartmongoInstance) { | ||||
|     await smartmongoInstance.stopAndDumpToDir( | ||||
|       `.nogit/dbdump/test.cursor.ts`, | ||||
|     ); | ||||
|   } | ||||
|   // Ensure process exits after cleanup | ||||
|   setTimeout(() => process.exit(), 2000); | ||||
| }); | ||||
|  | ||||
| export default tap.start(); | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartdata', | ||||
|   version: '5.15.0', | ||||
|   version: '5.15.1', | ||||
|   description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.' | ||||
| } | ||||
|   | ||||
| @@ -276,38 +276,27 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * get cursor | ||||
|    * @returns | ||||
|    */ | ||||
|   /** | ||||
|    * Get a cursor for streaming results, with optional session | ||||
|    * Get a cursor for streaming results, with optional session and native cursor modifiers. | ||||
|    * @param filterArg Partial filter to apply | ||||
|    * @param opts Optional session and modifier for the raw MongoDB cursor | ||||
|    */ | ||||
|   public static async getCursor<T>( | ||||
|     this: plugins.tsclass.typeFest.Class<T>, | ||||
|     filterArg: plugins.tsclass.typeFest.PartialDeep<T>, | ||||
|     opts?: { session?: plugins.mongodb.ClientSession } | ||||
|   ) { | ||||
|     const collection: SmartdataCollection<T> = (this as any).collection; | ||||
|     const cursor: SmartdataDbCursor<T> = await collection.getCursor( | ||||
|       convertFilterForMongoDb(filterArg), | ||||
|       this as any as typeof SmartDataDbDoc, | ||||
|       { session: opts?.session }, | ||||
|     ); | ||||
|     return cursor; | ||||
|   } | ||||
|  | ||||
|   public static async getCursorExtended<T>( | ||||
|     this: plugins.tsclass.typeFest.Class<T>, | ||||
|     filterArg: plugins.tsclass.typeFest.PartialDeep<T>, | ||||
|     modifierFunction = (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => cursorArg, | ||||
|     opts?: { | ||||
|       session?: plugins.mongodb.ClientSession; | ||||
|       modifier?: (cursorArg: plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>) => plugins.mongodb.FindCursor<plugins.mongodb.WithId<plugins.mongodb.BSON.Document>>; | ||||
|     } | ||||
|   ): Promise<SmartdataDbCursor<T>> { | ||||
|     const collection: SmartdataCollection<T> = (this as any).collection; | ||||
|     const { session, modifier } = opts || {}; | ||||
|     await collection.init(); | ||||
|     let cursor: plugins.mongodb.FindCursor<any> = collection.mongoDbCollection.find( | ||||
|       convertFilterForMongoDb(filterArg), | ||||
|     ); | ||||
|     cursor = modifierFunction(cursor); | ||||
|     return new SmartdataDbCursor<T>(cursor, this as any as typeof SmartDataDbDoc); | ||||
|     let rawCursor: plugins.mongodb.FindCursor<any> = | ||||
|       collection.mongoDbCollection.find(convertFilterForMongoDb(filterArg), { session }); | ||||
|     if (modifier) { | ||||
|       rawCursor = modifier(rawCursor); | ||||
|     } | ||||
|     return new SmartdataDbCursor<T>(rawCursor, this as any as typeof SmartDataDbDoc); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user