fix(db operations): Update transaction API to consistently pass optional session parameters across database operations
This commit is contained in:
parent
0806d3749b
commit
3ae2a7fcf5
@ -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.
|
||||
|
||||
|
19
readme.md
19
readme.md
@ -409,19 +409,23 @@ class Product extends SmartDataDbDoc<Product, Product> {
|
||||
|
||||
### 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<Order, Order> {
|
||||
throw new Error('Order cannot be deleted');
|
||||
}
|
||||
}
|
||||
// Called after deleting the document
|
||||
async afterDelete() {
|
||||
// Cleanup or audit actions
|
||||
await auditLogDeletion(this.id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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.'
|
||||
}
|
||||
|
@ -222,29 +222,34 @@ export class SmartdataCollection<T> {
|
||||
/**
|
||||
* finds an object in the DbCollection
|
||||
*/
|
||||
public async findOne(filterObject: any): Promise<any> {
|
||||
public async findOne(
|
||||
filterObject: any,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<any> {
|
||||
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<SmartdataDbCursor<any>> {
|
||||
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<any[]> {
|
||||
public async findAll(
|
||||
filterObject: any,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<any[]> {
|
||||
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<T> {
|
||||
/**
|
||||
* create an object in the database
|
||||
*/
|
||||
public async insert(dbDocArg: T & SmartDataDbDoc<T, unknown>): Promise<any> {
|
||||
public async insert(
|
||||
dbDocArg: T & SmartDataDbDoc<T, unknown>,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<any> {
|
||||
await this.init();
|
||||
await this.checkDoc(dbDocArg);
|
||||
this.markUniqueIndexes(dbDocArg.uniqueIndexes);
|
||||
@ -287,14 +295,17 @@ export class SmartdataCollection<T> {
|
||||
}
|
||||
|
||||
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<T, unknown>): Promise<any> {
|
||||
public async update(
|
||||
dbDocArg: T & SmartDataDbDoc<T, unknown>,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<any> {
|
||||
await this.init();
|
||||
await this.checkDoc(dbDocArg);
|
||||
const identifiableObject = await dbDocArg.createIdentifiableObject();
|
||||
@ -309,21 +320,27 @@ export class SmartdataCollection<T> {
|
||||
const result = await this.mongoDbCollection.updateOne(
|
||||
identifiableObject,
|
||||
{ $set: updateableObject },
|
||||
{ upsert: true },
|
||||
{ upsert: true, session: opts?.session },
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async delete(dbDocArg: T & SmartDataDbDoc<T, unknown>): Promise<any> {
|
||||
public async delete(
|
||||
dbDocArg: T & SmartDataDbDoc<T, unknown>,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<any> {
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
||||
|
@ -11,8 +11,18 @@ import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
|
||||
* - validate: post-fetch validator, return true to keep a doc
|
||||
*/
|
||||
export interface SearchOptions<T> {
|
||||
/**
|
||||
* Additional MongoDB filter to AND‐merge into the query
|
||||
*/
|
||||
filter?: Record<string, any>;
|
||||
/**
|
||||
* Post‐fetch validator; return true to keep each doc
|
||||
*/
|
||||
validate?: (doc: T) => Promise<boolean> | boolean;
|
||||
/**
|
||||
* Optional MongoDB session for transactional operations
|
||||
*/
|
||||
session?: plugins.mongodb.ClientSession;
|
||||
}
|
||||
|
||||
export type TDocCreation = 'db' | 'new' | 'mixed';
|
||||
@ -193,8 +203,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
public static async getInstances<T>(
|
||||
this: plugins.tsclass.typeFest.Class<T>,
|
||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<T[]> {
|
||||
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<T extends TImplements, TImplements, TManager extends
|
||||
public static async getInstance<T>(
|
||||
this: plugins.tsclass.typeFest.Class<T>,
|
||||
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
|
||||
opts?: { session?: plugins.mongodb.ClientSession }
|
||||
): Promise<T> {
|
||||
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<T extends TImplements, TImplements, TManager extends
|
||||
* get cursor
|
||||
* @returns
|
||||
*/
|
||||
/**
|
||||
* Get a cursor for streaming results, with optional session
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@ -339,7 +364,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
if (opts?.filter) {
|
||||
mongoFilter = { $and: [mongoFilter, opts.filter] };
|
||||
}
|
||||
let docs: T[] = await (this as any).getInstances(mongoFilter);
|
||||
// Fetch with optional session for transactions
|
||||
// Fetch within optional session
|
||||
let docs: T[] = await (this as any).getInstances(mongoFilter, { session: opts?.session });
|
||||
if (opts?.validate) {
|
||||
const out: T[] = [];
|
||||
for (const d of docs) {
|
||||
@ -546,10 +573,9 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* saves this instance but not any connected items
|
||||
* may lead to data inconsistencies, but is faster
|
||||
* saves this instance (optionally within a transaction)
|
||||
*/
|
||||
public async save() {
|
||||
public async save(opts?: { session?: plugins.mongodb.ClientSession }) {
|
||||
// allow hook before saving
|
||||
if (typeof (this as any).beforeSave === 'function') {
|
||||
await (this as any).beforeSave();
|
||||
@ -562,10 +588,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
// perform insert or update
|
||||
switch (this.creationStatus) {
|
||||
case 'db':
|
||||
dbResult = await this.collection.update(self);
|
||||
dbResult = await this.collection.update(self, { session: opts?.session });
|
||||
break;
|
||||
case 'new':
|
||||
dbResult = await this.collection.insert(self);
|
||||
dbResult = await this.collection.insert(self, { session: opts?.session });
|
||||
this.creationStatus = 'db';
|
||||
break;
|
||||
default:
|
||||
@ -579,15 +605,15 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a document from the database
|
||||
* deletes a document from the database (optionally within a transaction)
|
||||
*/
|
||||
public async delete() {
|
||||
public async delete(opts?: { session?: plugins.mongodb.ClientSession }) {
|
||||
// allow hook before deleting
|
||||
if (typeof (this as any).beforeDelete === 'function') {
|
||||
await (this as any).beforeDelete();
|
||||
}
|
||||
// perform deletion
|
||||
const result = await this.collection.delete(this);
|
||||
const result = await this.collection.delete(this, { session: opts?.session });
|
||||
// allow hook after delete
|
||||
if (typeof (this as any).afterDelete === 'function') {
|
||||
await (this as any).afterDelete();
|
||||
|
Loading…
x
Reference in New Issue
Block a user