fix(ci & formatting): Minor fixes: update CI workflow image and npmci package references, adjust package.json and readme URLs, and apply consistent code formatting.

This commit is contained in:
2025-04-06 18:18:39 +00:00
parent 4fac974fc9
commit 9426a21a2a
19 changed files with 437 additions and 354 deletions

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartdata',
version: '5.5.0',
version: '5.5.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

@ -49,7 +49,7 @@ export interface IManager {
db: SmartdataDb;
}
export const setDefaultManagerForDoc = <T>(managerArg: IManager, dbDocArg: T): T => {
export const setDefaultManagerForDoc = <T,>(managerArg: IManager, dbDocArg: T): T => {
(dbDocArg as any).prototype.defaultManager = managerArg;
return dbDocArg;
};
@ -190,7 +190,7 @@ export class SmartdataCollection<T> {
public async getCursor(
filterObjectArg: any,
dbDocArg: typeof SmartDataDbDoc
dbDocArg: typeof SmartDataDbDoc,
): Promise<SmartdataDbCursor<any>> {
await this.init();
const cursor = this.mongoDbCollection.find(filterObjectArg);
@ -213,7 +213,7 @@ export class SmartdataCollection<T> {
*/
public async watch(
filterObject: any,
smartdataDbDocArg: typeof SmartDataDbDoc
smartdataDbDocArg: typeof SmartDataDbDoc,
): Promise<SmartdataDbWatcher> {
await this.init();
const changeStream = this.mongoDbCollection.watch(
@ -224,7 +224,7 @@ export class SmartdataCollection<T> {
],
{
fullDocument: 'updateLookup',
}
},
);
const smartdataWatcher = new SmartdataDbWatcher(changeStream, smartdataDbDocArg);
await smartdataWatcher.readyDeferred.promise;
@ -261,7 +261,7 @@ export class SmartdataCollection<T> {
const result = await this.mongoDbCollection.updateOne(
identifiableObject,
{ $set: updateableObject },
{ upsert: true }
{ upsert: true },
);
return result;
}

View File

@ -17,7 +17,7 @@ export class SmartdataDbCursor<T = any> {
public async next(closeAtEnd = true) {
const result = this.smartdataDbDoc.createInstanceFromMongoDbNativeDoc(
await this.mongodbCursor.next()
await this.mongodbCursor.next(),
);
if (!result && closeAtEnd) {
await this.close();

View File

@ -139,7 +139,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
const eligibleLeader = leaders.find(
(leader) =>
leader.data.lastUpdated >=
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 20 })
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 20 }),
);
return eligibleLeader;
});
@ -178,16 +178,14 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
console.log('bidding code stored.');
});
console.log(`bidding for leadership...`);
await plugins.smartdelay.delayFor(
plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 })
);
await plugins.smartdelay.delayFor(plugins.smarttime.getMilliSecondsFromUnits({ seconds: 5 }));
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
let biddingInstances = await DistributedClass.getInstances({});
biddingInstances = biddingInstances.filter(
(instanceArg) =>
instanceArg.data.status === 'bidding' &&
instanceArg.data.lastUpdated >=
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 10 })
Date.now() - plugins.smarttime.getMilliSecondsFromUnits({ seconds: 10 }),
);
console.log(`found ${biddingInstances.length} bidding instances...`);
this.ownInstance.data.elected = true;
@ -242,7 +240,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
for (const instance of allInstances) {
if (instance.data.status === 'stopped') {
await instance.delete();
};
}
}
await plugins.smartdelay.delayFor(10000);
}
@ -250,7 +248,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
// abstract implemented methods
public async fireDistributedTaskRequest(
taskRequestArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
taskRequestArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest,
): Promise<plugins.taskbuffer.distributedCoordination.IDistributedTaskRequestResult> {
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
if (!this.ownInstance) {
@ -277,7 +275,7 @@ export class SmartdataDistributedCoordinator extends plugins.taskbuffer.distribu
}
public async updateDistributedTaskRequest(
infoBasisArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest
infoBasisArg: plugins.taskbuffer.distributedCoordination.IDistributedTaskRequest,
): Promise<void> {
await this.asyncExecutionStack.getExclusiveExecutionSlot(async () => {
const existingInfoBasis = this.ownInstance.data.taskRequests.find((infoBasisItem) => {

View File

@ -40,13 +40,13 @@ export function svDb() {
export function searchable() {
return (target: SmartDataDbDoc<unknown, unknown>, 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<string>());
}
// Add the property to the searchable fields set
searchableFieldsMap.get(className).add(key);
};
@ -135,7 +135,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
// STATIC
public static createInstanceFromMongoDbNativeDoc<T>(
this: plugins.tsclass.typeFest.Class<T>,
mongoDbNativeDocArg: any
mongoDbNativeDocArg: any,
): T {
const newInstance = new this();
(newInstance as any).creationStatus = 'db';
@ -153,7 +153,7 @@ 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>
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
): Promise<T[]> {
const foundDocs = await (this as any).collection.findAll(convertFilterForMongoDb(filterArg));
const returnArray = [];
@ -172,7 +172,7 @@ 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>
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
): Promise<T> {
const foundDoc = await (this as any).collection.findOne(convertFilterForMongoDb(filterArg));
if (foundDoc) {
@ -186,7 +186,10 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
/**
* get a unique id prefixed with the class name
*/
public static async getNewId<T = any>(this: plugins.tsclass.typeFest.Class<T>, lengthArg: number = 20) {
public static async getNewId<T = any>(
this: plugins.tsclass.typeFest.Class<T>,
lengthArg: number = 20,
) {
return `${(this as any).className}:${plugins.smartunique.shortId(lengthArg)}`;
}
@ -196,12 +199,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static async getCursor<T>(
this: plugins.tsclass.typeFest.Class<T>,
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
) {
const collection: SmartdataCollection<T> = (this as any).collection;
const cursor: SmartdataDbCursor<T> = await collection.getCursor(
convertFilterForMongoDb(filterArg),
this as any as typeof SmartDataDbDoc
this as any as typeof SmartDataDbDoc,
);
return cursor;
}
@ -214,12 +217,12 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static async watch<T>(
this: plugins.tsclass.typeFest.Class<T>,
filterArg: plugins.tsclass.typeFest.PartialDeep<T>
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
) {
const collection: SmartdataCollection<T> = (this as any).collection;
const watcher: SmartdataDbWatcher<T> = await collection.watch(
convertFilterForMongoDb(filterArg),
this as any
this as any,
);
return watcher;
}
@ -231,7 +234,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
public static async forEach<T>(
this: plugins.tsclass.typeFest.Class<T>,
filterArg: plugins.tsclass.typeFest.PartialDeep<T>,
forEachFunction: (itemArg: T) => Promise<any>
forEachFunction: (itemArg: T) => Promise<any>,
) {
const cursor: SmartdataDbCursor<T> = await (this as any).getCursor(filterArg);
await cursor.forEach(forEachFunction);
@ -242,7 +245,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static async getCount<T>(
this: plugins.tsclass.typeFest.Class<T>,
filterArg: plugins.tsclass.typeFest.PartialDeep<T> = ({} as any)
filterArg: plugins.tsclass.typeFest.PartialDeep<T> = {} as any,
) {
const collection: SmartdataCollection<T> = (this as any).collection;
return await collection.getCount(filterArg);
@ -255,15 +258,15 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static createSearchFilter<T>(
this: plugins.tsclass.typeFest.Class<T>,
luceneQuery: string
luceneQuery: string,
): any {
const className = (this as any).className || this.name;
const searchableFields = getSearchableFields(className);
if (searchableFields.length === 0) {
throw new Error(`No searchable fields defined for class ${className}`);
}
const adapter = new SmartdataLuceneAdapter(searchableFields);
return adapter.convert(luceneQuery);
}
@ -275,7 +278,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static async search<T>(
this: plugins.tsclass.typeFest.Class<T>,
luceneQuery: string
luceneQuery: string,
): Promise<T[]> {
const filter = (this as any).createSearchFilter(luceneQuery);
return await (this as any).getInstances(filter);
@ -288,25 +291,29 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
public static async searchWithLucene<T>(
this: plugins.tsclass.typeFest.Class<T>,
luceneQuery: string
luceneQuery: string,
): Promise<T[]> {
try {
const className = (this as any).className || this.name;
const searchableFields = getSearchableFields(className);
if (searchableFields.length === 0) {
console.warn(`No searchable fields defined for class ${className}, falling back to simple search`);
console.warn(
`No searchable fields defined for class ${className}, falling back to simple search`,
);
return (this as any).searchByTextAcrossFields(luceneQuery);
}
// Simple term search optimization
if (!luceneQuery.includes(':') &&
!luceneQuery.includes(' AND ') &&
!luceneQuery.includes(' OR ') &&
!luceneQuery.includes(' NOT ')) {
if (
!luceneQuery.includes(':') &&
!luceneQuery.includes(' AND ') &&
!luceneQuery.includes(' OR ') &&
!luceneQuery.includes(' NOT ')
) {
return (this as any).searchByTextAcrossFields(luceneQuery);
}
// Try to use the Lucene-to-MongoDB conversion
const filter = (this as any).createSearchFilter(luceneQuery);
return await (this as any).getInstances(filter);
@ -323,21 +330,21 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
*/
private static async searchByTextAcrossFields<T>(
this: plugins.tsclass.typeFest.Class<T>,
searchText: string
searchText: string,
): Promise<T[]> {
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 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);
@ -345,16 +352,15 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
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)) {
if (value && typeof value === 'string' && value.toLowerCase().includes(lowerSearchText)) {
return true;
}
}
@ -377,13 +383,13 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
* updated from db in any case where doc comes from db
*/
@globalSvDb()
_createdAt: string = (new Date()).toISOString();
_createdAt: string = new Date().toISOString();
/**
* will be updated everytime the doc is saved
*/
@globalSvDb()
_updatedAt: string = (new Date()).toISOString();
_updatedAt: string = new Date().toISOString();
/**
* an array of saveable properties of ALL doc
@ -424,7 +430,7 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
const self: any = this;
let dbResult: any;
this._updatedAt = (new Date()).toISOString();
this._updatedAt = new Date().toISOString();
switch (this.creationStatus) {
case 'db':
@ -480,10 +486,7 @@ 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
]
const saveableProperties = [...this.globalSaveableProperties, ...this.saveableProperties];
for (const propertyNameString of saveableProperties) {
saveableObject[propertyNameString] = this[propertyNameString];
}
@ -500,4 +503,4 @@ export class SmartDataDbDoc<T extends TImplements, TImplements, TManager extends
}
return identifiableObject;
}
}
}

View File

@ -41,7 +41,7 @@ export class EasyStore<T> {
private async getEasyStore(): Promise<InstanceType<typeof this.easyStoreClass>> {
if (this.easyStorePromise) {
return this.easyStorePromise;
};
}
// first run from here
const deferred = plugins.smartpromise.defer<InstanceType<typeof this.easyStoreClass>>();

View File

@ -4,7 +4,17 @@
import * as plugins from './plugins.js';
// Types
type NodeType = 'TERM' | 'PHRASE' | 'FIELD' | 'AND' | 'OR' | 'NOT' | 'RANGE' | 'WILDCARD' | 'FUZZY' | 'GROUP';
type NodeType =
| 'TERM'
| 'PHRASE'
| 'FIELD'
| 'AND'
| 'OR'
| 'NOT'
| 'RANGE'
| 'WILDCARD'
| 'FUZZY'
| 'GROUP';
interface QueryNode {
type: NodeType;
@ -59,7 +69,15 @@ interface GroupNode extends QueryNode {
value: AnyQueryNode;
}
type AnyQueryNode = TermNode | PhraseNode | FieldNode | BooleanNode | RangeNode | WildcardNode | FuzzyNode | GroupNode;
type AnyQueryNode =
| TermNode
| PhraseNode
| FieldNode
| BooleanNode
| RangeNode
| WildcardNode
| FuzzyNode
| GroupNode;
/**
* Lucene query parser
@ -68,9 +86,9 @@ export class LuceneParser {
private pos: number = 0;
private input: string = '';
private tokens: string[] = [];
constructor() {}
/**
* Parse a Lucene query string into an AST
*/
@ -78,24 +96,24 @@ export class LuceneParser {
this.input = query.trim();
this.pos = 0;
this.tokens = this.tokenize(this.input);
return this.parseQuery();
}
/**
* Tokenize the input string into tokens
*/
private tokenize(input: string): string[] {
const specialChars = /[()\[\]{}"~^:]/;
const operators = /AND|OR|NOT|TO/;
let tokens: string[] = [];
let current = '';
let inQuote = false;
for (let i = 0; i < input.length; i++) {
const char = input[i];
// Handle quoted strings
if (char === '"') {
if (inQuote) {
@ -109,12 +127,12 @@ export class LuceneParser {
}
continue;
}
if (inQuote) {
current += char;
continue;
}
// Handle whitespace
if (char === ' ' || char === '\t' || char === '\n') {
if (current) {
@ -123,7 +141,7 @@ export class LuceneParser {
}
continue;
}
// Handle special characters
if (specialChars.test(char)) {
if (current) {
@ -133,38 +151,37 @@ export class LuceneParser {
tokens.push(char);
continue;
}
current += char;
// Check if current is an operator
if (operators.test(current) &&
(i + 1 === input.length || /\s/.test(input[i + 1]))) {
if (operators.test(current) && (i + 1 === input.length || /\s/.test(input[i + 1]))) {
tokens.push(current);
current = '';
}
}
if (current) tokens.push(current);
return tokens;
}
/**
* Parse the main query expression
*/
private parseQuery(): AnyQueryNode {
const left = this.parseBooleanOperand();
if (this.pos < this.tokens.length) {
const token = this.tokens[this.pos];
if (token === 'AND' || token === 'OR') {
this.pos++;
const right = this.parseQuery();
return {
type: token as 'AND' | 'OR',
left,
right
right,
};
} else if (token === 'NOT' || token === '-') {
this.pos++;
@ -172,14 +189,14 @@ export class LuceneParser {
return {
type: 'NOT',
left,
right
right,
};
}
}
return left;
}
/**
* Parse boolean operands (terms, phrases, fields, groups)
*/
@ -187,14 +204,14 @@ export class LuceneParser {
if (this.pos >= this.tokens.length) {
throw new Error('Unexpected end of input');
}
const token = this.tokens[this.pos];
// Handle grouping with parentheses
if (token === '(') {
this.pos++;
const group = this.parseQuery();
if (this.pos < this.tokens.length && this.tokens[this.pos] === ')') {
this.pos++;
return { type: 'GROUP', value: group } as GroupNode;
@ -202,12 +219,12 @@ export class LuceneParser {
throw new Error('Unclosed group');
}
}
// Handle fields (field:value)
if (this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1] === ':') {
const field = token;
this.pos += 2; // Skip field and colon
if (this.pos < this.tokens.length) {
const value = this.parseBooleanOperand();
return { type: 'FIELD', field, value } as FieldNode;
@ -215,17 +232,17 @@ export class LuceneParser {
throw new Error('Expected value after field');
}
}
// Handle range queries
if (token === '[' || token === '{') {
return this.parseRange();
}
// Handle phrases ("term term")
if (token.startsWith('"') && token.endsWith('"')) {
const phrase = token.slice(1, -1);
this.pos++;
// Check for proximity operator
let proximity: number | undefined;
if (this.pos < this.tokens.length && this.tokens[this.pos] === '~') {
@ -237,64 +254,64 @@ export class LuceneParser {
throw new Error('Expected number after proximity operator');
}
}
return { type: 'PHRASE', value: phrase, proximity } as PhraseNode;
}
// Handle wildcards
if (token.includes('*') || token.includes('?')) {
this.pos++;
return { type: 'WILDCARD', value: token } as WildcardNode;
}
// Handle fuzzy searches
if (this.pos + 1 < this.tokens.length && this.tokens[this.pos + 1] === '~') {
const term = token;
this.pos += 2; // Skip term and tilde
let maxEdits = 2; // Default
if (this.pos < this.tokens.length && /^\d+$/.test(this.tokens[this.pos])) {
maxEdits = parseInt(this.tokens[this.pos], 10);
this.pos++;
}
return { type: 'FUZZY', value: term, maxEdits } as FuzzyNode;
}
// Simple term
this.pos++;
return { type: 'TERM', value: token } as TermNode;
}
/**
* Parse range queries
*/
private parseRange(): RangeNode {
const includeLower = this.tokens[this.pos] === '[';
const includeUpper = this.tokens[this.pos + 4] === ']';
this.pos++; // Skip open bracket
if (this.pos + 4 >= this.tokens.length) {
throw new Error('Invalid range query syntax');
}
const lower = this.tokens[this.pos];
this.pos++;
if (this.tokens[this.pos] !== 'TO') {
throw new Error('Expected TO in range query');
}
this.pos++;
const upper = this.tokens[this.pos];
this.pos++;
if (this.tokens[this.pos] !== (includeLower ? ']' : '}')) {
throw new Error('Invalid range query closing bracket');
}
this.pos++;
// For simplicity, assuming the field is handled separately
return {
type: 'RANGE',
@ -302,7 +319,7 @@ export class LuceneParser {
lower,
upper,
includeLower,
includeUpper
includeUpper,
};
}
}
@ -313,7 +330,7 @@ export class LuceneParser {
*/
export class LuceneToMongoTransformer {
constructor() {}
/**
* Transform a Lucene AST node to a MongoDB query
*/
@ -343,7 +360,7 @@ export class LuceneToMongoTransformer {
throw new Error(`Unsupported node type: ${(node as any).type}`);
}
}
/**
* Transform a term to MongoDB query
* FIXED: properly structured $or query for multiple fields
@ -352,17 +369,17 @@ export class LuceneToMongoTransformer {
// If specific fields are provided, search across those fields
if (searchFields && searchFields.length > 0) {
// Create an $or query to search across multiple fields
const orConditions = searchFields.map(field => ({
[field]: { $regex: node.value, $options: 'i' }
const orConditions = searchFields.map((field) => ({
[field]: { $regex: node.value, $options: 'i' },
}));
return { $or: orConditions };
}
// Otherwise, use text search (requires a text index on desired fields)
return { $text: { $search: node.value } };
}
/**
* Transform a phrase to MongoDB query
* FIXED: properly structured $or query for multiple fields
@ -370,17 +387,17 @@ export class LuceneToMongoTransformer {
private transformPhrase(node: PhraseNode, searchFields?: string[]): any {
// If specific fields are provided, search phrase across those fields
if (searchFields && searchFields.length > 0) {
const orConditions = searchFields.map(field => ({
[field]: { $regex: `${node.value.replace(/\s+/g, '\\s+')}`, $options: 'i' }
const orConditions = searchFields.map((field) => ({
[field]: { $regex: `${node.value.replace(/\s+/g, '\\s+')}`, $options: 'i' },
}));
return { $or: orConditions };
}
// For phrases, we use a regex to ensure exact matches
return { $text: { $search: `"${node.value}"` } };
}
/**
* Transform a field query to MongoDB query
*/
@ -391,50 +408,50 @@ export class LuceneToMongoTransformer {
rangeNode.field = node.field;
return this.transformRange(rangeNode);
}
// Handle special case for wildcards on fields
if (node.value.type === 'WILDCARD') {
return {
[node.field]: {
return {
[node.field]: {
$regex: this.luceneWildcardToRegex((node.value as WildcardNode).value),
$options: 'i'
}
$options: 'i',
},
};
}
// Handle special case for fuzzy searches on fields
if (node.value.type === 'FUZZY') {
return {
[node.field]: {
return {
[node.field]: {
$regex: this.createFuzzyRegex((node.value as FuzzyNode).value),
$options: 'i'
}
$options: 'i',
},
};
}
// Special case for exact term matches on fields
if (node.value.type === 'TERM') {
return { [node.field]: { $regex: (node.value as TermNode).value, $options: 'i' } };
}
// Special case for phrase matches on fields
if (node.value.type === 'PHRASE') {
return {
[node.field]: {
return {
[node.field]: {
$regex: `${(node.value as PhraseNode).value.replace(/\s+/g, '\\s+')}`,
$options: 'i'
}
$options: 'i',
},
};
}
// For other cases, we'll transform the value and apply it to the field
const transformedValue = this.transform(node.value);
// If the transformed value uses $text, we need to adapt it for the field
if (transformedValue.$text) {
return { [node.field]: { $regex: transformedValue.$text.$search, $options: 'i' } };
}
// Handle $or and $and cases
if (transformedValue.$or || transformedValue.$and) {
// This is a bit complex - we need to restructure the query to apply the field
@ -444,10 +461,10 @@ export class LuceneToMongoTransformer {
return { [node.field]: { $regex: term, $options: 'i' } };
}
}
return { [node.field]: transformedValue };
}
/**
* Extract a term from a boolean query (simplification)
*/
@ -460,7 +477,7 @@ export class LuceneToMongoTransformer {
}
}
}
if (query.$and && Array.isArray(query.$and) && query.$and.length > 0) {
const firstClause = query.$and[0];
for (const field in firstClause) {
@ -469,10 +486,10 @@ export class LuceneToMongoTransformer {
}
}
}
return null;
}
/**
* Transform AND operator to MongoDB query
* FIXED: $and must be an array
@ -480,7 +497,7 @@ export class LuceneToMongoTransformer {
private transformAnd(node: BooleanNode): any {
return { $and: [this.transform(node.left), this.transform(node.right)] };
}
/**
* Transform OR operator to MongoDB query
* FIXED: $or must be an array
@ -488,7 +505,7 @@ export class LuceneToMongoTransformer {
private transformOr(node: BooleanNode): any {
return { $or: [this.transform(node.left), this.transform(node.right)] };
}
/**
* Transform NOT operator to MongoDB query
* FIXED: $and must be an array and $not usage
@ -496,43 +513,40 @@ export class LuceneToMongoTransformer {
private transformNot(node: BooleanNode): any {
const leftQuery = this.transform(node.left);
const rightQuery = this.transform(node.right);
// Create a query that includes left but excludes right
if (rightQuery.$text) {
// For text searches, we need a different approach
// We'll use a negated regex instead
const searchTerm = rightQuery.$text.$search.replace(/"/g, '');
// Determine the fields to apply the negation to
const notConditions = [];
for (const field in leftQuery) {
if (field !== '$or' && field !== '$and') {
notConditions.push({
[field]: { $not: { $regex: searchTerm, $options: 'i' } }
[field]: { $not: { $regex: searchTerm, $options: 'i' } },
});
}
}
// If left query has $or or $and, we need to handle it differently
if (leftQuery.$or) {
return {
$and: [
leftQuery,
{ $nor: [{ $or: notConditions }] }
]
$and: [leftQuery, { $nor: [{ $or: notConditions }] }],
};
} else {
// Simple case - just add $not to each field
return {
$and: [leftQuery, { $and: notConditions }]
$and: [leftQuery, { $and: notConditions }],
};
}
} else {
// For other queries, we can use $not directly
// We need to handle different structures based on the rightQuery
let notQuery = {};
if (rightQuery.$or) {
notQuery = { $nor: rightQuery.$or };
} else if (rightQuery.$and) {
@ -544,28 +558,28 @@ export class LuceneToMongoTransformer {
notQuery[field] = { $not: rightQuery[field] };
}
}
return { $and: [leftQuery, notQuery] };
}
}
/**
* Transform range query to MongoDB query
*/
private transformRange(node: RangeNode): any {
const range: any = {};
if (node.lower !== '*') {
range[node.includeLower ? '$gte' : '$gt'] = this.parseValue(node.lower);
}
if (node.upper !== '*') {
range[node.includeUpper ? '$lte' : '$lt'] = this.parseValue(node.upper);
}
return { [node.field]: range };
}
/**
* Transform wildcard query to MongoDB query
* FIXED: properly structured for multiple fields
@ -573,20 +587,20 @@ export class LuceneToMongoTransformer {
private transformWildcard(node: WildcardNode, searchFields?: string[]): any {
// Convert Lucene wildcards to MongoDB regex
const regex = this.luceneWildcardToRegex(node.value);
// If specific fields are provided, search wildcard across those fields
if (searchFields && searchFields.length > 0) {
const orConditions = searchFields.map(field => ({
[field]: { $regex: regex, $options: 'i' }
const orConditions = searchFields.map((field) => ({
[field]: { $regex: regex, $options: 'i' },
}));
return { $or: orConditions };
}
// By default, apply to the default field
return { $regex: regex, $options: 'i' };
}
/**
* Transform fuzzy query to MongoDB query
* FIXED: properly structured for multiple fields
@ -595,20 +609,20 @@ export class LuceneToMongoTransformer {
// MongoDB doesn't have built-in fuzzy search
// This is a very basic approach using regex
const regex = this.createFuzzyRegex(node.value);
// If specific fields are provided, search fuzzy term across those fields
if (searchFields && searchFields.length > 0) {
const orConditions = searchFields.map(field => ({
[field]: { $regex: regex, $options: 'i' }
const orConditions = searchFields.map((field) => ({
[field]: { $regex: regex, $options: 'i' },
}));
return { $or: orConditions };
}
// By default, apply to the default field
return { $regex: regex, $options: 'i' };
}
/**
* Convert Lucene wildcards to MongoDB regex patterns
*/
@ -622,7 +636,7 @@ export class LuceneToMongoTransformer {
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
}
/**
* Create a simplified fuzzy search regex
*/
@ -639,7 +653,7 @@ export class LuceneToMongoTransformer {
}
return regex;
}
/**
* Parse string values to appropriate types (numbers, dates, etc.)
*/
@ -648,17 +662,17 @@ export class LuceneToMongoTransformer {
if (/^-?\d+$/.test(value)) {
return parseInt(value, 10);
}
if (/^-?\d+\.\d+$/.test(value)) {
return parseFloat(value);
}
// Try to parse as date (simplified)
const date = new Date(value);
if (!isNaN(date.getTime())) {
return date;
}
// Default to string
return value;
}
@ -671,7 +685,7 @@ export class SmartdataLuceneAdapter {
private parser: LuceneParser;
private transformer: LuceneToMongoTransformer;
private defaultSearchFields: string[] = [];
/**
* @param defaultSearchFields - Optional array of field names to search across when no field is specified
*/
@ -682,7 +696,7 @@ export class SmartdataLuceneAdapter {
this.defaultSearchFields = defaultSearchFields;
}
}
/**
* Convert a Lucene query string to a MongoDB query object
* @param luceneQuery - The Lucene query string to convert
@ -691,32 +705,33 @@ export class SmartdataLuceneAdapter {
convert(luceneQuery: string, searchFields?: string[]): any {
try {
// For simple single term queries, create a simpler query structure
if (!luceneQuery.includes(':') &&
!luceneQuery.includes(' AND ') &&
!luceneQuery.includes(' OR ') &&
!luceneQuery.includes(' NOT ') &&
!luceneQuery.includes('(') &&
!luceneQuery.includes('[')) {
if (
!luceneQuery.includes(':') &&
!luceneQuery.includes(' AND ') &&
!luceneQuery.includes(' OR ') &&
!luceneQuery.includes(' NOT ') &&
!luceneQuery.includes('(') &&
!luceneQuery.includes('[')
) {
// This is a simple term, use a more direct approach
const fieldsToSearch = searchFields || this.defaultSearchFields;
if (fieldsToSearch && fieldsToSearch.length > 0) {
return {
$or: fieldsToSearch.map(field => ({
[field]: { $regex: luceneQuery, $options: 'i' }
}))
$or: fieldsToSearch.map((field) => ({
[field]: { $regex: luceneQuery, $options: 'i' },
})),
};
}
}
// For more complex queries, use the full parser
// Parse the Lucene query into an AST
const ast = this.parser.parse(luceneQuery);
// Use provided searchFields, fall back to defaultSearchFields
const fieldsToSearch = searchFields || this.defaultSearchFields;
// Transform the AST to a MongoDB query
return this.transformWithFields(ast, fieldsToSearch);
} catch (error) {
@ -724,18 +739,22 @@ export class SmartdataLuceneAdapter {
throw new Error(`Failed to convert Lucene query: ${error}`);
}
}
/**
* Helper method to transform the AST with field information
*/
private transformWithFields(node: AnyQueryNode, searchFields: string[]): any {
// Special case for term nodes without a specific field
if (node.type === 'TERM' || node.type === 'PHRASE' ||
node.type === 'WILDCARD' || node.type === 'FUZZY') {
if (
node.type === 'TERM' ||
node.type === 'PHRASE' ||
node.type === 'WILDCARD' ||
node.type === 'FUZZY'
) {
return this.transformer.transform(node, searchFields);
}
// For other node types, use the standard transformation
return this.transformer.transform(node);
}
}
}

View File

@ -14,7 +14,7 @@ export class SmartdataDbWatcher<T = any> {
public changeSubject = new plugins.smartrx.rxjs.Subject<T>();
constructor(
changeStreamArg: plugins.mongodb.ChangeStream<T>,
smartdataDbDocArg: typeof SmartDataDbDoc
smartdataDbDocArg: typeof SmartDataDbDoc,
) {
this.changeStream = changeStreamArg;
this.changeStream.on('change', async (item: any) => {
@ -23,7 +23,7 @@ export class SmartdataDbWatcher<T = any> {
return;
}
this.changeSubject.next(
smartdataDbDocArg.createInstanceFromMongoDbNativeDoc(item.fullDocument) as any as T
smartdataDbDocArg.createInstanceFromMongoDbNativeDoc(item.fullDocument) as any as T,
);
});
plugins.smartdelay.delayFor(0).then(() => {

View File

@ -11,4 +11,4 @@ export { convenience };
// to be removed with the next breaking update
import type * as plugins from './plugins.js';
type IMongoDescriptor = plugins.tsclass.database.IMongoDescriptor;
export type { IMongoDescriptor };
export type { IMongoDescriptor };