From 43f9033cccacdf8986c06a01a6d6e3a2d78b2004 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 24 Apr 2025 11:34:49 +0000 Subject: [PATCH] fix(cursor): Improve cursor usage documentation and refactor getCursor API to support native cursor modifiers --- changelog.md | 7 +++ readme.md | 39 ++++++++-------- test/test.cursor.ts | 97 ++++++++++++++++++++++++++++++++++++++++ ts/00_commitinfo_data.ts | 2 +- ts/classes.doc.ts | 39 ++++++---------- 5 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 test/test.cursor.ts diff --git a/changelog.md b/changelog.md index 184228a..3948acd 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/readme.md b/readme.md index 2397b8a..e0578b3 100644 --- a/readme.md +++ b/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 diff --git a/test/test.cursor.ts b/test/test.cursor.ts new file mode 100644 index 0000000..23b1bb4 --- /dev/null +++ b/test/test.cursor.ts @@ -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 { + @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(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 6ab90b2..641b448 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/classes.doc.ts b/ts/classes.doc.ts index 37d000c..620fc77 100644 --- a/ts/classes.doc.ts +++ b/ts/classes.doc.ts @@ -276,38 +276,27 @@ export class SmartDataDbDoc( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep, - opts?: { session?: plugins.mongodb.ClientSession } - ) { - const collection: SmartdataCollection = (this as any).collection; - const cursor: SmartdataDbCursor = await collection.getCursor( - convertFilterForMongoDb(filterArg), - this as any as typeof SmartDataDbDoc, - { session: opts?.session }, - ); - return cursor; - } - - public static async getCursorExtended( - this: plugins.tsclass.typeFest.Class, - filterArg: plugins.tsclass.typeFest.PartialDeep, - modifierFunction = (cursorArg: plugins.mongodb.FindCursor>) => cursorArg, + opts?: { + session?: plugins.mongodb.ClientSession; + modifier?: (cursorArg: plugins.mongodb.FindCursor>) => plugins.mongodb.FindCursor>; + } ): Promise> { const collection: SmartdataCollection = (this as any).collection; + const { session, modifier } = opts || {}; await collection.init(); - let cursor: plugins.mongodb.FindCursor = collection.mongoDbCollection.find( - convertFilterForMongoDb(filterArg), - ); - cursor = modifierFunction(cursor); - return new SmartdataDbCursor(cursor, this as any as typeof SmartDataDbDoc); + let rawCursor: plugins.mongodb.FindCursor = + collection.mongoDbCollection.find(convertFilterForMongoDb(filterArg), { session }); + if (modifier) { + rawCursor = modifier(rawCursor); + } + return new SmartdataDbCursor(rawCursor, this as any as typeof SmartDataDbDoc); } /**