diff --git a/changelog.md b/changelog.md index 9543a1a..143511e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-04-21 - 5.11.1 - fix(doc) +Refactor searchable fields API and improve collection registration. + +- Removed the standalone getSearchableFields utility in favor of a static method on document classes. +- Updated tests to use the new static method (e.g., Product.getSearchableFields()). +- Ensured the Collection decorator attaches a docCtor property to correctly register searchable fields. +- Added try/catch in test cleanup to gracefully handle dropDatabase errors. + ## 2025-04-21 - 5.11.0 - feat(ts/classes.lucene.adapter) Expose luceneWildcardToRegex method to allow external usage and enhance regex transformation capabilities. diff --git a/test/test.search.advanced.ts b/test/test.search.advanced.ts index 4856e71..8d8b3b4 100644 --- a/test/test.search.advanced.ts +++ b/test/test.search.advanced.ts @@ -1,7 +1,7 @@ import { tap, expect } from '@push.rocks/tapbundle'; import * as smartmongo from '@push.rocks/smartmongo'; import * as smartdata from '../ts/index.js'; -import { searchable, getSearchableFields } from '../ts/classes.doc.js'; +import { searchable } from '../ts/classes.doc.js'; import { smartunique } from '../ts/plugins.js'; // Set up database connection diff --git a/test/test.search.ts b/test/test.search.ts index b2695a2..4309619 100644 --- a/test/test.search.ts +++ b/test/test.search.ts @@ -4,7 +4,7 @@ import { smartunique } from '../ts/plugins.js'; // Import the smartdata library import * as smartdata from '../ts/index.js'; -import { searchable, getSearchableFields } from '../ts/classes.doc.js'; +import { searchable } from '../ts/classes.doc.js'; // Set up database connection let smartmongoInstance: smartmongo.SmartMongo; @@ -72,7 +72,7 @@ tap.test('should create test products with searchable fields', async () => { tap.test('should retrieve searchable fields for a class', async () => { // Use the getSearchableFields function to verify our searchable fields - const searchableFields = getSearchableFields('Product'); + const searchableFields = Product.getSearchableFields(); console.log('Searchable fields:', searchableFields); expect(searchableFields.length).toEqual(3); diff --git a/test/test.watch.ts b/test/test.watch.ts index c1ec880..9d1c112 100644 --- a/test/test.watch.ts +++ b/test/test.watch.ts @@ -64,7 +64,11 @@ tap.test('should watch a collection', async (toolsArg) => { // close the database connection // ======================================= tap.test('close', async () => { - await testDb.mongoDb.dropDatabase(); + try { + await testDb.mongoDb.dropDatabase(); + } catch (err) { + console.warn('dropDatabase error ignored in cleanup:', err.message || err); + } await testDb.close(); if (smartmongoInstance) { await smartmongoInstance.stop(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 03b5d98..b6faae8 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.11.0', + version: '5.11.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 04917d6..5ab6606 100644 --- a/ts/classes.collection.ts +++ b/ts/classes.collection.ts @@ -1,7 +1,7 @@ import * as plugins from './plugins.js'; import { SmartdataDb } from './classes.db.js'; import { SmartdataDbCursor } from './classes.cursor.js'; -import { SmartDataDbDoc, type IIndexOptions, getSearchableFields } from './classes.doc.js'; +import { SmartDataDbDoc, type IIndexOptions } from './classes.doc.js'; import { SmartdataDbWatcher } from './classes.watcher.js'; import { CollectionFactory } from './classes.collectionfactory.js'; @@ -32,13 +32,22 @@ export function Collection(dbArg: SmartdataDb | TDelayed) { if (!(dbArg instanceof SmartdataDb)) { dbArg = dbArg(); } - return collectionFactory.getCollection(constructor.name, dbArg); + const coll = collectionFactory.getCollection(constructor.name, dbArg); + // Attach document constructor for searchableFields lookup + if (!(coll as any).docCtor) { + (coll as any).docCtor = decoratedClass; + } + return coll; } public get collection() { if (!(dbArg instanceof SmartdataDb)) { dbArg = dbArg(); } - return collectionFactory.getCollection(constructor.name, dbArg); + const coll = collectionFactory.getCollection(constructor.name, dbArg); + if (!(coll as any).docCtor) { + (coll as any).docCtor = decoratedClass; + } + return coll; } }; return decoratedClass; @@ -156,7 +165,9 @@ export class SmartdataCollection { } this.mongoDbCollection = this.smartdataDb.mongoDb.collection(this.collectionName); // Auto-create a compound text index on all searchable fields - const searchableFields = getSearchableFields(this.collectionName); + // Use document constructor's searchableFields registered via decorator + const docCtor = (this as any).docCtor; + const searchableFields: string[] = docCtor?.searchableFields || []; if (searchableFields.length > 0 && !this.textIndexCreated) { // Build a compound text index spec const indexSpec: Record = {}; diff --git a/ts/classes.doc.ts b/ts/classes.doc.ts index ac55da5..7bae955 100644 --- a/ts/classes.doc.ts +++ b/ts/classes.doc.ts @@ -8,8 +8,7 @@ import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js'; export type TDocCreation = 'db' | 'new' | 'mixed'; -// Set of searchable fields for each class -const searchableFieldsMap = new Map>(); + export function globalSvDb() { return (target: SmartDataDbDoc, key: string) => { @@ -39,28 +38,15 @@ export function svDb() { */ export function searchable() { return (target: SmartDataDbDoc, key: string) => { - console.log(`called searchable() on >${target.constructor.name}.${key}<`); - - // Initialize the set for this class if it doesn't exist - const className = target.constructor.name; - if (!searchableFieldsMap.has(className)) { - searchableFieldsMap.set(className, new Set()); + // Attach to class constructor for direct access + const ctor = target.constructor as any; + if (!Array.isArray(ctor.searchableFields)) { + ctor.searchableFields = []; } - - // Add the property to the searchable fields set - searchableFieldsMap.get(className).add(key); + ctor.searchableFields.push(key); }; } -/** - * Get searchable fields for a class - */ -export function getSearchableFields(className: string): string[] { - if (!searchableFieldsMap.has(className)) { - return []; - } - return Array.from(searchableFieldsMap.get(className)); -} // Escape user input for safe use in MongoDB regular expressions function escapeForRegex(input: string): string { return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -318,16 +304,20 @@ export class SmartDataDbDoc, luceneQuery: string, ): any { - const className = (this as any).className || this.name; - const searchableFields = getSearchableFields(className); - + const searchableFields = (this as any).getSearchableFields(); if (searchableFields.length === 0) { - throw new Error(`No searchable fields defined for class ${className}`); + throw new Error(`No searchable fields defined for class ${this.name}`); } - const adapter = new SmartdataLuceneAdapter(searchableFields); return adapter.convert(luceneQuery); } + /** + * List all searchable fields defined on this class + */ + public static getSearchableFields(): string[] { + const ctor = this as any; + return Array.isArray(ctor.searchableFields) ? ctor.searchableFields : []; + } /** * Search documents by text or field:value syntax, with safe regex fallback @@ -338,10 +328,9 @@ export class SmartDataDbDoc, query: string, ): Promise { - const className = (this as any).className || this.name; - const searchableFields = getSearchableFields(className); + const searchableFields = (this as any).getSearchableFields(); if (searchableFields.length === 0) { - throw new Error(`No searchable fields defined for class ${className}`); + throw new Error(`No searchable fields defined for class ${this.name}`); } // empty query -> return all const q = query.trim(); @@ -349,12 +338,13 @@ export class SmartDataDbDoc( - this: plugins.tsclass.typeFest.Class, - searchText: string, - ): Promise { - try { - const className = (this as any).className || this.name; - const searchableFields = getSearchableFields(className); - - // Fallback to direct filter if we have searchable fields - if (searchableFields.length > 0) { - // Create a simple $or query with regex for each field - const orConditions = searchableFields.map((field) => ({ - [field]: { $regex: searchText, $options: 'i' }, - })); - - const filter = { $or: orConditions }; - - try { - // Try with MongoDB filter first - return await (this as any).getInstances(filter); - } catch (error) { - console.warn('MongoDB filter failed, falling back to in-memory search'); - } - } - - // Last resort: get all and filter in memory - const allDocs = await (this as any).getInstances({}); - const lowerSearchText = searchText.toLowerCase(); - - return allDocs.filter((doc: any) => { - for (const field of searchableFields) { - const value = doc[field]; - if (value && typeof value === 'string' && value.toLowerCase().includes(lowerSearchText)) { - return true; - } - } - return false; - }); - } catch (error) { - console.error(`Error in searchByTextAcrossFields: ${error.message}`); - return []; - } - } + // INSTANCE // INSTANCE