Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
e7c0951786 | |||
efc107907c | |||
2b8b0e5bdd | |||
3ae2a7fcf5 | |||
0806d3749b | |||
f5d5e20a97 |
23
changelog.md
23
changelog.md
@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-24 - 5.15.0 - feat(svDb)
|
||||
Enhance svDb decorator to support custom serialization and deserialization options
|
||||
|
||||
- Added an optional options parameter to the svDb decorator to accept serialize/deserialize functions
|
||||
- Updated instance creation logic (updateFromDb) to apply custom deserialization if provided
|
||||
- Updated createSavableObject to use custom serialization when available
|
||||
|
||||
## 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.
|
||||
|
||||
- Calls beforeSave hook if defined before performing insert or update.
|
||||
- Calls afterSave hook after a document is saved.
|
||||
- Calls beforeDelete hook before deletion and afterDelete hook afterward.
|
||||
- Ensures _updatedAt timestamp is refreshed during save operations.
|
||||
|
||||
## 2025-04-22 - 5.13.1 - fix(search)
|
||||
Improve search query parsing for implicit AND queries by preserving quoted substrings and better handling free terms, quoted phrases, and field:value tokens.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartdata",
|
||||
"version": "5.13.1",
|
||||
"version": "5.15.0",
|
||||
"private": false,
|
||||
"description": "An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
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.13.1',
|
||||
version: '5.15.0',
|
||||
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';
|
||||
@ -29,16 +39,34 @@ export function globalSvDb() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for custom serialization/deserialization of a field.
|
||||
*/
|
||||
export interface SvDbOptions {
|
||||
/** Function to serialize the field value before saving to DB */
|
||||
serialize?: (value: any) => any;
|
||||
/** Function to deserialize the field value after reading from DB */
|
||||
deserialize?: (value: any) => any;
|
||||
}
|
||||
|
||||
/**
|
||||
* saveable - saveable decorator to be used on class properties
|
||||
*/
|
||||
export function svDb() {
|
||||
export function svDb(options?: SvDbOptions) {
|
||||
return (target: SmartDataDbDoc<unknown, unknown>, key: string) => {
|
||||
console.log(`called svDb() on >${target.constructor.name}.${key}<`);
|
||||
if (!target.saveableProperties) {
|
||||
target.saveableProperties = [];
|
||||
}
|
||||
target.saveableProperties.push(key);
|
||||
// attach custom serializer/deserializer options to the class constructor
|
||||
const ctor = target.constructor as any;
|
||||
if (!ctor._svDbOptions) {
|
||||
ctor._svDbOptions = {};
|
||||
}
|
||||
if (options) {
|
||||
ctor._svDbOptions[key] = options;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -179,7 +207,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
const newInstance = new this();
|
||||
(newInstance as any).creationStatus = 'db';
|
||||
for (const key of Object.keys(mongoDbNativeDocArg)) {
|
||||
newInstance[key] = mongoDbNativeDocArg[key];
|
||||
const rawValue = mongoDbNativeDocArg[key];
|
||||
const optionsMap = (this as any)._svDbOptions || {};
|
||||
const opts = optionsMap[key];
|
||||
newInstance[key] = opts && typeof opts.deserialize === 'function'
|
||||
? opts.deserialize(rawValue)
|
||||
: rawValue;
|
||||
}
|
||||
return newInstance;
|
||||
}
|
||||
@ -193,8 +226,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 +250,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 +279,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 +387,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,35 +596,52 @@ 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();
|
||||
}
|
||||
// tslint:disable-next-line: no-this-assignment
|
||||
const self: any = this;
|
||||
let dbResult: any;
|
||||
|
||||
// update timestamp
|
||||
this._updatedAt = new Date().toISOString();
|
||||
|
||||
// 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:
|
||||
console.error('neither new nor in db?');
|
||||
}
|
||||
// allow hook after saving
|
||||
if (typeof (this as any).afterSave === 'function') {
|
||||
await (this as any).afterSave();
|
||||
}
|
||||
return dbResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes a document from the database
|
||||
* deletes a document from the database (optionally within a transaction)
|
||||
*/
|
||||
public async delete() {
|
||||
await this.collection.delete(this);
|
||||
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, { session: opts?.session });
|
||||
// allow hook after delete
|
||||
if (typeof (this as any).afterDelete === 'function') {
|
||||
await (this as any).afterDelete();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -601,7 +668,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
public async updateFromDb() {
|
||||
const mongoDbNativeDoc = await this.collection.findOne(await this.createIdentifiableObject());
|
||||
for (const key of Object.keys(mongoDbNativeDoc)) {
|
||||
this[key] = mongoDbNativeDoc[key];
|
||||
const rawValue = mongoDbNativeDoc[key];
|
||||
const optionsMap = (this.constructor as any)._svDbOptions || {};
|
||||
const opts = optionsMap[key];
|
||||
this[key] = opts && typeof opts.deserialize === 'function'
|
||||
? opts.deserialize(rawValue)
|
||||
: rawValue;
|
||||
}
|
||||
}
|
||||
|
||||
@ -611,8 +683,14 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
|
||||
public async createSavableObject(): Promise<TImplements> {
|
||||
const saveableObject: unknown = {}; // is not exposed to outside, so any is ok here
|
||||
const saveableProperties = [...this.globalSaveableProperties, ...this.saveableProperties];
|
||||
// apply custom serialization if configured
|
||||
const optionsMap = (this.constructor as any)._svDbOptions || {};
|
||||
for (const propertyNameString of saveableProperties) {
|
||||
saveableObject[propertyNameString] = this[propertyNameString];
|
||||
const rawValue = (this as any)[propertyNameString];
|
||||
const opts = optionsMap[propertyNameString];
|
||||
(saveableObject as any)[propertyNameString] = opts && typeof opts.serialize === 'function'
|
||||
? opts.serialize(rawValue)
|
||||
: rawValue;
|
||||
}
|
||||
return saveableObject as TImplements;
|
||||
}
|
||||
|
Reference in New Issue
Block a user