Compare commits

...

10 Commits

Author SHA1 Message Date
39d2957b7d 5.13.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 3m5s
Default (tags) / release (push) Failing after 52s
Default (tags) / metadata (push) Successful in 57s
2025-04-22 20:34:23 +00:00
490524516e feat(search): Improve search query handling and update documentation 2025-04-22 20:34:23 +00:00
ccd4b9e1ec 5.12.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 3m7s
Default (tags) / release (push) Failing after 52s
Default (tags) / metadata (push) Successful in 1m2s
2025-04-22 20:09:21 +00:00
9c6d6d9f2c fix(search): Fix handling of quoted wildcard patterns in field-specific search queries and add tests for location-based wildcard phrase searches 2025-04-22 20:09:21 +00:00
e4d787096e 5.12.1
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Successful in 3m7s
Default (tags) / release (push) Failing after 52s
Default (tags) / metadata (push) Successful in 1m0s
2025-04-22 19:37:50 +00:00
2bf923b4f1 fix(search): Improve implicit AND logic for mixed free term and field queries in search and enhance wildcard field handling. 2025-04-22 19:37:50 +00:00
0ca1d452b4 5.12.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 3m9s
Default (tags) / release (push) Failing after 53s
Default (tags) / metadata (push) Successful in 58s
2025-04-22 19:13:17 +00:00
436311ab06 feat(doc/search): Enhance search functionality with filter and validate options for advanced query control 2025-04-22 19:13:17 +00:00
498f586ddb 5.11.4
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Successful in 3m8s
Default (tags) / release (push) Failing after 52s
Default (tags) / metadata (push) Successful in 1m1s
2025-04-22 18:36:47 +00:00
6c50bd23ec fix(search): Implement implicit AND logic for mixed simple term and field:value queries in search 2025-04-22 18:36:47 +00:00
7 changed files with 346 additions and 12 deletions

View File

@ -1,5 +1,42 @@
# Changelog # Changelog
## 2025-04-22 - 5.13.0 - feat(search)
Improve search query handling and update documentation
- Added 'codex.md' providing a high-level project overview and detailed search API documentation.
- Enhanced search parsing in SmartDataDbDoc to support combined free-term and quoted field phrase queries.
- Introduced a new fallback branch in the search method to handle free term with quoted field input.
- Updated tests in test/test.search.ts to cover new combined query scenarios and ensure robust behavior.
## 2025-04-22 - 5.12.2 - fix(search)
Fix handling of quoted wildcard patterns in field-specific search queries and add tests for location-based wildcard phrase searches
- Strip surrounding quotes from wildcard patterns in field queries to correctly transform them to regex
- Introduce new tests in test/test.search.ts to validate exact quoted and unquoted wildcard searches on a location field
## 2025-04-22 - 5.12.1 - fix(search)
Improve implicit AND logic for mixed free term and field queries in search and enhance wildcard field handling.
- Updated regex for field:value parsing to capture full value with wildcards.
- Added explicit handling for free terms by converting to regex across searchable fields.
- Improved error messaging for attempts to search non-searchable fields.
- Extended tests to cover combined free term and wildcard field searches, including error cases.
## 2025-04-22 - 5.12.0 - feat(doc/search)
Enhance search functionality with filter and validate options for advanced query control
- Added 'filter' option to merge additional MongoDB query constraints in search
- Introduced 'validate' hook to post-process and filter fetched documents
- Refactored underlying execQuery function to support additional search options
- Updated tests to cover new search scenarios and fallback mechanisms
## 2025-04-22 - 5.11.4 - fix(search)
Implement implicit AND logic for mixed simple term and field:value queries in search
- Added a new branch to detect and handle search queries that mix field:value pairs with plain terms without explicit operators
- Builds an implicit $and filter when query parts contain colon(s) but lack explicit boolean operators or quotes
- Ensures proper parsing and improved robustness of search filters
## 2025-04-22 - 5.11.3 - fix(lucene adapter and search tests) ## 2025-04-22 - 5.11.3 - fix(lucene adapter and search tests)
Improve range query parsing in Lucene adapter and expand search test coverage Improve range query parsing in Lucene adapter and expand search test coverage

77
codex.md Normal file
View File

@ -0,0 +1,77 @@
# SmartData Project Overview
This document provides a high-level overview of the SmartData library (`@push.rocks/smartdata`), its architecture, core components, and key features—including recent enhancements to the search API.
## 1. Project Purpose
- A TypeScriptfirst wrapper around MongoDB that supplies:
- Stronglytyped document & collection classes
- Decoratorbased schema definition (no external schema files)
- Advanced search capabilities with Lucenestyle queries
- Builtin support for realtime data sync, distributed coordination, and keyvalue EasyStore
## 2. Core Concepts & Components
- **SmartDataDb**: Manages the MongoDB connection, pooling, and initialization of collections.
- **SmartDataDbDoc**: Base class for all document models; provides CRUD, upsert, and cursor APIs.
- **Decorators**:
- `@Collection`: Associates a class with a MongoDB collection
- `@svDb()`: Marks a field as persisted to the DB
- `@unI()`: Marks a field as a unique index
- `@index()`: Adds a regular index
- `@searchable()`: Marks a field for inclusion in text searches or regex queries
- **SmartdataCollection**: Wraps a MongoDB collection; autocreates indexes based on decorators.
- **Lucene Adapter**: Parses a Lucene query string into an AST and transforms it to a MongoDB filter object.
- **EasyStore**: A simple, schemaless keyvalue store built on top of MongoDB for sharing ephemeral data.
- **Distributed Coordinator**: Leader election and taskdistribution API for building resilient, multiinstance systems.
- **Watcher**: Listens to change streams for realtime updates and integrates with RxJS.
## 3. Search API
SmartData provides a unified `.search(query[, opts])` method on all models with `@searchable()` fields:
- **Supported Syntax**:
1. Exact field:value (e.g. `field:Value`)
2. Quoted phrases (e.g. `"exact phrase"` or `'exact phrase'`)
3. Wildcards: `*` (zero or more chars) and `?` (single char)
4. Boolean operators: `AND`, `OR`, `NOT`
5. Grouping: parenthesis `(A OR B) AND C`
6. Range queries: `[num TO num]`, `{num TO num}`
7. Multiterm unquoted: terms ANDd across all searchable fields
8. Empty query returns all documents
- **Fallback Mechanisms**:
1. Text index based `$text` search (if supported)
2. Fieldscoped and multifield regex queries
3. Inmemory filtering for complex or unsupported cases
### New Security & Extensibility Hooks
The `.search(query, opts?)` signature now accepts a `SearchOptions<T>` object:
```ts
interface SearchOptions<T> {
filter?: Record<string, any>; // Additional MongoDB filter ANDmerged
validate?: (doc: T) => boolean; // Postfetch hook to drop results
}
```
- **filter**: Enforces mandatory constraints (e.g. multitenant isolation) directly in the Mongo query.
- **validate**: An async function that runs after fetching; return `false` to exclude a document.
## 4. Testing Strategy
- Unit tests in `test/test.search.ts` cover basic search functionality and new options:
- Exact, wildcard, phrase, boolean and grouping cases
- Implicit AND and mixed freeterm + field searches
- Edge cases (nonsearchable fields, quoted wildcards, no matches)
- `filter` and `validate` tests ensure security hooks work as intended
- Advanced search scenarios are covered in `test/test.search.advanced.ts`.
## 5. Usage Example
```ts
// Basic search
const prods = await Product.search('wireless earbuds');
// Scoped search (only your organizations items)
const myItems = await Product.search('book', { filter: { ownerId } });
// Postsearch validation (only cheap items)
const cheapItems = await Product.search('', { validate: p => p.price < 50 });
```
---
Last updated: 2025-04-22

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartdata", "name": "@push.rocks/smartdata",
"version": "5.11.3", "version": "5.13.0",
"private": false, "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.", "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", "main": "dist_ts/index.js",

View File

@ -225,6 +225,10 @@ await Product.search('TypeScript Aufgabe');
// 7: Empty query returns all documents // 7: Empty query returns all documents
await Product.search(''); await Product.search('');
// 8: Scoped search with additional filter (e.g. multi-tenant isolation)
await Product.search('book', { filter: { ownerId: currentUserId } });
// 9: Post-search validation hook to drop unwanted results (e.g. price check)
await Product.search('', { validate: (p) => p.price < 100 });
``` ```
The search functionality includes: The search functionality includes:

View File

@ -9,6 +9,8 @@ import { searchable } from '../ts/classes.doc.js';
// Set up database connection // Set up database connection
let smartmongoInstance: smartmongo.SmartMongo; let smartmongoInstance: smartmongo.SmartMongo;
let testDb: smartdata.SmartdataDb; let testDb: smartdata.SmartdataDb;
// Class for location-based wildcard/phrase tests
let LocationDoc: any;
// Define a test class with searchable fields using the standard SmartDataDbDoc // Define a test class with searchable fields using the standard SmartDataDbDoc
@smartdata.Collection(() => testDb) @smartdata.Collection(() => testDb)
@ -276,6 +278,123 @@ tap.test('should support wildcard plain term with question mark pattern', async
expect(names).toEqual(['Galaxy S21', 'iPhone 12']); expect(names).toEqual(['Galaxy S21', 'iPhone 12']);
}); });
// Filter and Validation tests
tap.test('should apply filter option to restrict results', async () => {
// search term 'book' across all fields but restrict to Books category
const bookFiltered = await Product.search('book', { filter: { category: 'Books' } });
expect(bookFiltered.length).toEqual(2);
bookFiltered.forEach((p) => expect(p.category).toEqual('Books'));
});
tap.test('should apply validate hook to post-filter results', async () => {
// return only products with price > 500
const expensive = await Product.search('', { validate: (p) => p.price > 500 });
expect(expensive.length).toBeGreaterThan(0);
expensive.forEach((p) => expect(p.price).toBeGreaterThan(500));
});
// Tests for quoted and wildcard field-specific phrases
tap.test('setup location test products', async () => {
@smartdata.Collection(() => testDb)
class LD extends smartdata.SmartDataDbDoc<LD, LD> {
@smartdata.unI() public id: string = smartunique.shortId();
@smartdata.svDb() @searchable() public location: string;
constructor(loc: string) { super(); this.location = loc; }
}
// Assign to outer variable for subsequent tests
LocationDoc = LD;
const locations = ['Berlin', 'Frankfurt am Main', 'Frankfurt am Oder', 'London'];
for (const loc of locations) {
await new LocationDoc(loc).save();
}
});
tap.test('should search exact quoted field phrase', async () => {
const results = await (LocationDoc as any).search('location:"Frankfurt am Main"');
expect(results.length).toEqual(1);
expect(results[0].location).toEqual('Frankfurt am Main');
});
tap.test('should search wildcard quoted field phrase', async () => {
const results = await (LocationDoc as any).search('location:"Frankfurt am *"');
const names = results.map((d: any) => d.location).sort();
expect(names).toEqual(['Frankfurt am Main', 'Frankfurt am Oder']);
});
tap.test('should search unquoted wildcard field', async () => {
const results = await (LocationDoc as any).search('location:Frankfurt*');
const names = results.map((d: any) => d.location).sort();
expect(names).toEqual(['Frankfurt am Main', 'Frankfurt am Oder']);
});
// Combined free-term + field phrase/wildcard tests
let CombinedDoc: any;
tap.test('setup combined docs for free-term and location tests', async () => {
@smartdata.Collection(() => testDb)
class CD extends smartdata.SmartDataDbDoc<CD, CD> {
@smartdata.unI() public id: string = smartunique.shortId();
@smartdata.svDb() @searchable() public name: string;
@smartdata.svDb() @searchable() public location: string;
constructor(name: string, location: string) { super(); this.name = name; this.location = location; }
}
CombinedDoc = CD;
const docs = [
new CombinedDoc('TypeScript', 'Berlin'),
new CombinedDoc('TypeScript', 'Frankfurt am Main'),
new CombinedDoc('TypeScript', 'Frankfurt am Oder'),
new CombinedDoc('JavaScript', 'Berlin'),
];
for (const d of docs) await d.save();
});
tap.test('should search free term and exact quoted field phrase', async () => {
const res = await CombinedDoc.search('TypeScript location:"Berlin"');
expect(res.length).toEqual(1);
expect(res[0].location).toEqual('Berlin');
});
tap.test('should not match free term with non-matching quoted field phrase', async () => {
const res = await CombinedDoc.search('TypeScript location:"Frankfurt d"');
expect(res.length).toEqual(0);
});
tap.test('should search free term with quoted wildcard field phrase', async () => {
const res = await CombinedDoc.search('TypeScript location:"Frankfurt am *"');
const locs = res.map((r: any) => r.location).sort();
expect(locs).toEqual(['Frankfurt am Main', 'Frankfurt am Oder']);
});
// Quoted exact field phrase without wildcard should return no matches if no exact match
tap.test('should not match location:"Frankfurt d"', async () => {
const results = await (LocationDoc as any).search('location:"Frankfurt d"');
expect(results.length).toEqual(0);
});
// Combined free-term and field wildcard tests
tap.test('should combine free term and wildcard field search', async () => {
const results = await Product.search('book category:Book*');
expect(results.length).toEqual(2);
results.forEach((p) => expect(p.category).toEqual('Books'));
});
tap.test('should not match when free term matches but wildcard field does not', async () => {
const results = await Product.search('book category:Kitchen*');
expect(results.length).toEqual(0);
});
// Non-searchable field should cause an error for combined queries
tap.test('should throw when combining term with non-searchable field', async () => {
let error: Error;
try {
await Product.search('book location:Berlin');
} catch (e) {
error = e as Error;
}
expect(error).toBeTruthy();
expect(error.message).toMatch(/not searchable/);
});
tap.test('should throw when combining term with non-searchable wildcard field', async () => {
let error: Error;
try {
await Product.search('book location:Berlin*');
} catch (e) {
error = e as Error;
}
expect(error).toBeTruthy();
expect(error.message).toMatch(/not searchable/);
});
// Close database connection // Close database connection
tap.test('close database connection', async () => { tap.test('close database connection', async () => {
await testDb.mongoDb.dropDatabase(); await testDb.mongoDb.dropDatabase();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartdata', name: '@push.rocks/smartdata',
version: '5.11.3', version: '5.13.0',
description: 'An advanced library for NoSQL data organization and manipulation using TypeScript with support for MongoDB, data validation, collections, and custom data types.' 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

@ -5,6 +5,15 @@ import { SmartdataDbCursor } from './classes.cursor.js';
import { type IManager, SmartdataCollection } from './classes.collection.js'; import { type IManager, SmartdataCollection } from './classes.collection.js';
import { SmartdataDbWatcher } from './classes.watcher.js'; import { SmartdataDbWatcher } from './classes.watcher.js';
import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js'; import { SmartdataLuceneAdapter } from './classes.lucene.adapter.js';
/**
* Search options for `.search()`:
* - filter: additional MongoDB query to AND-merge
* - validate: post-fetch validator, return true to keep a doc
*/
export interface SearchOptions<T> {
filter?: Record<string, any>;
validate?: (doc: T) => Promise<boolean> | boolean;
}
export type TDocCreation = 'db' | 'new' | 'mixed'; export type TDocCreation = 'db' | 'new' | 'mixed';
@ -318,15 +327,40 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
const ctor = this as any; const ctor = this as any;
return Array.isArray(ctor.searchableFields) ? ctor.searchableFields : []; return Array.isArray(ctor.searchableFields) ? ctor.searchableFields : [];
} }
/**
* Execute a query with optional hard filter and post-fetch validation
*/
private static async execQuery<T>(
this: plugins.tsclass.typeFest.Class<T>,
baseFilter: Record<string, any>,
opts?: SearchOptions<T>
): Promise<T[]> {
let mongoFilter = baseFilter || {};
if (opts?.filter) {
mongoFilter = { $and: [mongoFilter, opts.filter] };
}
let docs: T[] = await (this as any).getInstances(mongoFilter);
if (opts?.validate) {
const out: T[] = [];
for (const d of docs) {
if (await opts.validate(d)) out.push(d);
}
docs = out;
}
return docs;
}
/** /**
* Search documents by text or field:value syntax, with safe regex fallback * Search documents by text or field:value syntax, with safe regex fallback
* Supports additional filtering and post-fetch validation via opts
* @param query A search term or field:value expression * @param query A search term or field:value expression
* @param opts Optional filter and validate hooks
* @returns Array of matching documents * @returns Array of matching documents
*/ */
public static async search<T>( public static async search<T>(
this: plugins.tsclass.typeFest.Class<T>, this: plugins.tsclass.typeFest.Class<T>,
query: string, query: string,
opts?: SearchOptions<T>,
): Promise<T[]> { ): Promise<T[]> {
const searchableFields = (this as any).getSearchableFields(); const searchableFields = (this as any).getSearchableFields();
if (searchableFields.length === 0) { if (searchableFields.length === 0) {
@ -335,7 +369,8 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
// empty query -> return all // empty query -> return all
const q = query.trim(); const q = query.trim();
if (!q) { if (!q) {
return await (this as any).getInstances({}); // empty query: fetch all, apply opts
return await (this as any).execQuery({}, opts);
} }
// simple exact field:value (no spaces, no wildcards, no quotes) // simple exact field:value (no spaces, no wildcards, no quotes)
// simple exact field:value (no spaces, wildcards, quotes) // simple exact field:value (no spaces, wildcards, quotes)
@ -346,30 +381,35 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
if (!searchableFields.includes(field)) { if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`); throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
} }
return await (this as any).getInstances({ [field]: value }); // simple field:value search
return await (this as any).execQuery({ [field]: value }, opts);
} }
// quoted phrase across all searchable fields: exact match of phrase // quoted phrase across all searchable fields: exact match of phrase
const quoted = q.match(/^"(.+)"$|^'(.+)'$/); const quoted = q.match(/^"(.+)"$|^'(.+)'$/);
if (quoted) { if (quoted) {
const phrase = quoted[1] || quoted[2] || ''; const phrase = quoted[1] || quoted[2] || '';
// build regex that matches the exact phrase (allowing flexible whitespace)
const parts = phrase.split(/\s+/).map((t) => escapeForRegex(t)); const parts = phrase.split(/\s+/).map((t) => escapeForRegex(t));
const pattern = parts.join('\\s+'); const pattern = parts.join('\\s+');
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } })); const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
return await (this as any).getInstances({ $or: orConds }); return await (this as any).execQuery({ $or: orConds }, opts);
} }
// wildcard field:value (supports * and ?) -> direct regex on that field // wildcard field:value (supports * and ?) -> direct regex on that field
const wildcardField = q.match(/^(\w+):(.+[*?].*)$/); const wildcardField = q.match(/^(\w+):(.+[*?].*)$/);
if (wildcardField) { if (wildcardField) {
const field = wildcardField[1]; const field = wildcardField[1];
const pattern = wildcardField[2]; // Support quoted wildcard patterns: strip surrounding quotes
let pattern = wildcardField[2];
if ((pattern.startsWith('"') && pattern.endsWith('"')) ||
(pattern.startsWith("'") && pattern.endsWith("'"))) {
pattern = pattern.slice(1, -1);
}
if (!searchableFields.includes(field)) { if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`); throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
} }
// escape regex special chars except * and ?, then convert wildcards // escape regex special chars except * and ?, then convert wildcards
const escaped = pattern.replace(/([.+^${}()|[\\]\\])/g, '\\$1'); const escaped = pattern.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.'); const regexPattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
return await (this as any).getInstances({ [field]: { $regex: regexPattern, $options: 'i' } }); return await (this as any).execQuery({ [field]: { $regex: regexPattern, $options: 'i' } }, opts);
} }
// wildcard plain term across all fields (supports * and ?) // wildcard plain term across all fields (supports * and ?)
if (!q.includes(':') && (q.includes('*') || q.includes('?'))) { if (!q.includes(':') && (q.includes('*') || q.includes('?'))) {
@ -377,13 +417,70 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
const escaped = q.replace(/([.+^${}()|[\\]\\])/g, '\\$1'); const escaped = q.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.'); const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } })); const orConds = searchableFields.map((f) => ({ [f]: { $regex: pattern, $options: 'i' } }));
return await (this as any).getInstances({ $or: orConds }); return await (this as any).execQuery({ $or: orConds }, opts);
}
// implicit AND: combine free terms and field:value terms (with or without wildcards)
const parts = q.split(/\s+/);
const hasColon = parts.some((t) => t.includes(':'));
if (
parts.length > 1 && hasColon &&
!q.includes(' AND ') && !q.includes(' OR ') && !q.includes(' NOT ') &&
!q.includes('(') && !q.includes(')') &&
!q.includes('[') && !q.includes(']') &&
!q.includes('"') && !q.includes("'")
) {
const andConds = parts.map((term) => {
const m = term.match(/^(\w+):(.+)$/);
if (m) {
const field = m[1];
const value = m[2];
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
if (value.includes('*') || value.includes('?')) {
// wildcard field search
const escaped = value.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
return { [field]: { $regex: pattern, $options: 'i' } };
}
// exact field:value
return { [field]: value };
}
// free term -> regex across all searchable fields
const esc = escapeForRegex(term);
return { $or: searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })) };
});
return await (this as any).execQuery({ $and: andConds }, opts);
}
// free term and quoted field phrase (exact or wildcard), e.g. 'term field:"phrase"' or 'term field:"ph*se"'
const freeWithQuotedField = q.match(/^(\S+)\s+(\w+):"(.+)"$/);
if (freeWithQuotedField) {
const freeTerm = freeWithQuotedField[1];
const field = freeWithQuotedField[2];
let phrase = freeWithQuotedField[3];
if (!searchableFields.includes(field)) {
throw new Error(`Field '${field}' is not searchable for class ${this.name}`);
}
// free term condition across all searchable fields
const freeEsc = escapeForRegex(freeTerm);
const freeCond = { $or: searchableFields.map((f) => ({ [f]: { $regex: freeEsc, $options: 'i' } })) };
// field condition: exact match or wildcard pattern
let fieldCond;
if (phrase.includes('*') || phrase.includes('?')) {
const escaped = phrase.replace(/([.+^${}()|[\\]\\])/g, '\\$1');
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
fieldCond = { [field]: { $regex: pattern, $options: 'i' } };
} else {
fieldCond = { [field]: phrase };
}
return await (this as any).execQuery({ $and: [freeCond, fieldCond] }, opts);
} }
// detect advanced Lucene syntax: field:value, wildcards, boolean, grouping // detect advanced Lucene syntax: field:value, wildcards, boolean, grouping
const luceneSyntax = /(\w+:[^\s]+)|\*|\?|\bAND\b|\bOR\b|\bNOT\b|\(|\)/; const luceneSyntax = /(\w+:[^\s]+)|\*|\?|\bAND\b|\bOR\b|\bNOT\b|\(|\)/;
if (luceneSyntax.test(q)) { if (luceneSyntax.test(q)) {
const filter = (this as any).createSearchFilter(q); const filter = (this as any).createSearchFilter(q);
return await (this as any).getInstances(filter); return await (this as any).execQuery(filter, opts);
} }
// multi-term unquoted -> AND of regex across fields for each term // multi-term unquoted -> AND of regex across fields for each term
const terms = q.split(/\s+/); const terms = q.split(/\s+/);
@ -393,12 +490,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })); const ors = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
return { $or: ors }; return { $or: ors };
}); });
return await (this as any).getInstances({ $and: andConds }); return await (this as any).execQuery({ $and: andConds }, opts);
} }
// single term -> regex across all searchable fields // single term -> regex across all searchable fields
const esc = escapeForRegex(q); const esc = escapeForRegex(q);
const orConds = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } })); const orConds = searchableFields.map((f) => ({ [f]: { $regex: esc, $options: 'i' } }));
return await (this as any).getInstances({ $or: orConds }); return await (this as any).execQuery({ $or: orConds }, opts);
} }