fix(db operations): Update transaction API to consistently pass optional session parameters across database operations

This commit is contained in:
2025-04-23 17:28:49 +00:00
parent 0806d3749b
commit 3ae2a7fcf5
6 changed files with 99 additions and 33 deletions

View File

@ -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.'
}

View File

@ -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 });
}
/**

View File

@ -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

View File

@ -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 ANDmerge into the query
*/
filter?: Record<string, any>;
/**
* Postfetch 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();