diff --git a/changelog.md b/changelog.md index a389412..71e55ee 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-04-23 - 5.14.1 - fix(db operations) +Update transaction API to consistently pass optional session parameters across database operations + +- Revised transaction support in readme to use startSession without await and showcased session usage in getInstance and save calls +- Updated methods in classes.collection.ts to accept an optional session parameter for findOne, getCursor, findAll, insert, update, delete, and getCount +- Enhanced SmartDataDbDoc save and delete methods to propagate session parameters +- Improved overall consistency of transactional APIs across the library + ## 2025-04-23 - 5.14.0 - feat(doc) Implement support for beforeSave, afterSave, beforeDelete, and afterDelete lifecycle hooks in document save and delete operations to allow custom logic execution during these critical moments. diff --git a/readme.md b/readme.md index 185c928..2397b8a 100644 --- a/readme.md +++ b/readme.md @@ -409,19 +409,23 @@ class Product extends SmartDataDbDoc { ### Transaction Support -Use MongoDB transactions for atomic operations: +Use MongoDB transactions for atomic operations. SmartData now exposes `startSession()` and accepts an optional session in all fetch and write APIs: ```typescript -const session = await db.startSession(); +// start a client session (no await) +const session = db.startSession(); try { + // wrap operations in a transaction await session.withTransaction(async () => { - const user = await User.getInstance({ id: 'user-id' }, { session }); + // pass session as second arg to getInstance + const user = await User.getInstance({ id: 'user-id' }, session); user.balance -= 100; + // pass session in save opts await user.save({ session }); - const recipient = await User.getInstance({ id: 'recipient-id' }, { session }); + const recipient = await User.getInstance({ id: 'recipient-id' }, session); recipient.balance += 100; - await user.save({ session }); + await recipient.save({ session }); }); } finally { await session.endSession(); @@ -518,6 +522,11 @@ class Order extends SmartDataDbDoc { throw new Error('Order cannot be deleted'); } } + // Called after deleting the document + async afterDelete() { + // Cleanup or audit actions + await auditLogDeletion(this.id); + } } ``` diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index d3dce4d..653646c 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.14.0', + version: '5.14.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.collection.ts b/ts/classes.collection.ts index 5ab6606..a3b2c76 100644 --- a/ts/classes.collection.ts +++ b/ts/classes.collection.ts @@ -222,29 +222,34 @@ export class SmartdataCollection { /** * finds an object in the DbCollection */ - public async findOne(filterObject: any): Promise { + public async findOne( + filterObject: any, + opts?: { session?: plugins.mongodb.ClientSession } + ): Promise { await this.init(); - const cursor = this.mongoDbCollection.find(filterObject); - const result = await cursor.next(); - cursor.close(); - return result; + // Use MongoDB driver's findOne with optional session + return this.mongoDbCollection.findOne(filterObject, { session: opts?.session }); } public async getCursor( filterObjectArg: any, dbDocArg: typeof SmartDataDbDoc, + opts?: { session?: plugins.mongodb.ClientSession } ): Promise> { await this.init(); - const cursor = this.mongoDbCollection.find(filterObjectArg); + const cursor = this.mongoDbCollection.find(filterObjectArg, { session: opts?.session }); return new SmartdataDbCursor(cursor, dbDocArg); } /** * finds an object in the DbCollection */ - public async findAll(filterObject: any): Promise { + public async findAll( + filterObject: any, + opts?: { session?: plugins.mongodb.ClientSession } + ): Promise { await this.init(); - const cursor = this.mongoDbCollection.find(filterObject); + const cursor = this.mongoDbCollection.find(filterObject, { session: opts?.session }); const result = await cursor.toArray(); cursor.close(); return result; @@ -276,7 +281,10 @@ export class SmartdataCollection { /** * create an object in the database */ - public async insert(dbDocArg: T & SmartDataDbDoc): Promise { + public async insert( + dbDocArg: T & SmartDataDbDoc, + opts?: { session?: plugins.mongodb.ClientSession } + ): Promise { await this.init(); await this.checkDoc(dbDocArg); this.markUniqueIndexes(dbDocArg.uniqueIndexes); @@ -287,14 +295,17 @@ export class SmartdataCollection { } const saveableObject = await dbDocArg.createSavableObject(); - const result = await this.mongoDbCollection.insertOne(saveableObject); + const result = await this.mongoDbCollection.insertOne(saveableObject, { session: opts?.session }); return result; } /** * inserts object into the DbCollection */ - public async update(dbDocArg: T & SmartDataDbDoc): Promise { + public async update( + dbDocArg: T & SmartDataDbDoc, + opts?: { session?: plugins.mongodb.ClientSession } + ): Promise { await this.init(); await this.checkDoc(dbDocArg); const identifiableObject = await dbDocArg.createIdentifiableObject(); @@ -309,21 +320,27 @@ export class SmartdataCollection { const result = await this.mongoDbCollection.updateOne( identifiableObject, { $set: updateableObject }, - { upsert: true }, + { upsert: true, session: opts?.session }, ); return result; } - public async delete(dbDocArg: T & SmartDataDbDoc): Promise { + public async delete( + dbDocArg: T & SmartDataDbDoc, + opts?: { session?: plugins.mongodb.ClientSession } + ): Promise { await this.init(); await this.checkDoc(dbDocArg); const identifiableObject = await dbDocArg.createIdentifiableObject(); - await this.mongoDbCollection.deleteOne(identifiableObject); + await this.mongoDbCollection.deleteOne(identifiableObject, { session: opts?.session }); } - public async getCount(filterObject: any) { + public async getCount( + filterObject: any, + opts?: { session?: plugins.mongodb.ClientSession } + ) { await this.init(); - return this.mongoDbCollection.countDocuments(filterObject); + return this.mongoDbCollection.countDocuments(filterObject, { session: opts?.session }); } /** diff --git a/ts/classes.db.ts b/ts/classes.db.ts index 20df5c0..9716628 100644 --- a/ts/classes.db.ts +++ b/ts/classes.db.ts @@ -63,6 +63,12 @@ export class SmartdataDb { this.status = 'disconnected'; logger.log('info', `disconnected from database ${this.smartdataOptions.mongoDbName}`); } + /** + * Start a MongoDB client session for transactions + */ + public startSession(): plugins.mongodb.ClientSession { + return this.mongoDbClient.startSession(); + } // handle table to class distribution diff --git a/ts/classes.doc.ts b/ts/classes.doc.ts index 5356220..2254063 100644 --- a/ts/classes.doc.ts +++ b/ts/classes.doc.ts @@ -11,8 +11,18 @@ import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js'; * - validate: post-fetch validator, return true to keep a doc */ export interface SearchOptions { + /** + * Additional MongoDB filter to AND‐merge into the query + */ filter?: Record; + /** + * Post‐fetch validator; return true to keep each doc + */ validate?: (doc: T) => Promise | boolean; + /** + * Optional MongoDB session for transactional operations + */ + session?: plugins.mongodb.ClientSession; } export type TDocCreation = 'db' | 'new' | 'mixed'; @@ -193,8 +203,13 @@ export class SmartDataDbDoc( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep, + opts?: { session?: plugins.mongodb.ClientSession } ): Promise { - const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg)); + // Pass session through to findAll for transactional queries + const foundDocs = await (this as any).collection.findAll( + convertFilterForMongoDb(filterArg), + { session: opts?.session }, + ); const returnArray = []; for (const foundDoc of foundDocs) { const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc); @@ -212,8 +227,13 @@ export class SmartDataDbDoc( this: plugins.tsclass.typeFest.Class, filterArg: plugins.tsclass.typeFest.PartialDeep, + opts?: { session?: plugins.mongodb.ClientSession } ): Promise { - const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg)); + // Retrieve one document, with optional session for transactions + const foundDoc = await (this as any).collection.findOne( + convertFilterForMongoDb(filterArg), + { session: opts?.session }, + ); if (foundDoc) { const newInstance: T = (this as any).createInstanceFromMongoDbNativeDoc(foundDoc); return newInstance; @@ -236,14 +256,19 @@ 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; } @@ -339,7 +364,9 @@ export class SmartDataDbDoc