feat: Enhance QueryBuilder with operator normalization and null handling; update ShardManager to ensure shard index bounds; improve BaseModel tests for email validation

This commit is contained in:
anonpenguin 2025-06-19 12:54:21 +03:00
parent 9f425f2106
commit 0305cb1737
3 changed files with 79 additions and 56 deletions

View File

@ -28,22 +28,55 @@ export class QueryBuilder<T extends BaseModel> {
where(field: string, operatorOrValue: string | any, value?: any): this { where(field: string, operatorOrValue: string | any, value?: any): this {
if (value !== undefined) { if (value !== undefined) {
// Three parameter version: where('field', 'operator', 'value') // Three parameter version: where('field', 'operator', 'value')
this.conditions.push({ field, operator: operatorOrValue, value }); const normalizedOperator = this.normalizeOperator(operatorOrValue);
this.conditions.push({ field, operator: normalizedOperator, value });
} else { } else {
// Two parameter version: where('field', 'value') - defaults to equality // Two parameter version: where('field', 'value') - defaults to equality
// Special handling for null checks
if (typeof operatorOrValue === 'string') {
const lowerValue = operatorOrValue.toLowerCase();
if (lowerValue === 'is null' || lowerValue === 'is not null') {
this.conditions.push({ field, operator: lowerValue, value: null });
return this;
}
}
this.conditions.push({ field, operator: 'eq', value: operatorOrValue }); this.conditions.push({ field, operator: 'eq', value: operatorOrValue });
} }
return this; return this;
} }
private normalizeOperator(operator: string): string {
const operatorMap: { [key: string]: string } = {
'=': 'eq',
'!=': 'ne',
'<>': 'ne',
'>': 'gt',
'>=': 'gte',
'<': 'lt',
'<=': 'lte',
'like': 'like',
'ilike': 'ilike',
'in': 'in',
'not_in': 'not in', // Reverse mapping: internal -> expected
'not in': 'not in',
'is null': 'is null',
'is not null': 'is not null',
'regex': 'regex',
'between': 'between'
};
return operatorMap[operator.toLowerCase()] || operator;
}
whereIn(field: string, values: any[]): this { whereIn(field: string, values: any[]): this {
return this.where(field, 'in', values); return this.where(field, 'in', values);
} }
whereNotIn(field: string, values: any[]): this { whereNotIn(field: string, values: any[]): this {
return this.where(field, 'not_in', values); return this.where(field, 'not in', values);
} }
whereNull(field: string): this { whereNull(field: string): this {
this.conditions.push({ field, operator: 'is null', value: null }); this.conditions.push({ field, operator: 'is null', value: null });
return this; return this;
@ -126,17 +159,21 @@ export class QueryBuilder<T extends BaseModel> {
}); });
} else { } else {
// Simple orWhere version: orWhere('field', 'operator', 'value') or orWhere('field', 'value') // Simple orWhere version: orWhere('field', 'operator', 'value') or orWhere('field', 'value')
let finalOperator = '='; let finalOperator = 'eq';
let finalValue = operatorOrValue; let finalValue = operatorOrValue;
if (value !== undefined) { if (value !== undefined) {
finalOperator = operatorOrValue; finalOperator = this.normalizeOperator(operatorOrValue);
finalValue = value; finalValue = value;
} } else {
// Two parameter version: special handling for null checks
const lastCondition = this.conditions[this.conditions.length - 1]; if (typeof operatorOrValue === 'string') {
if (lastCondition) { const lowerValue = operatorOrValue.toLowerCase();
lastCondition.logical = 'or'; if (lowerValue === 'is null' || lowerValue === 'is not null') {
finalOperator = lowerValue;
finalValue = null;
}
}
} }
this.conditions.push({ this.conditions.push({
@ -258,6 +295,15 @@ export class QueryBuilder<T extends BaseModel> {
return await this.exec(); return await this.exec();
} }
async find(): Promise<T[]> {
return await this.exec();
}
async findOne(): Promise<T | null> {
const results = await this.limit(1).exec();
return results[0] || null;
}
async first(): Promise<T | null> { async first(): Promise<T | null> {
const results = await this.limit(1).exec(); const results = await this.limit(1).exec();
return results[0] || null; return results[0] || null;
@ -513,66 +559,35 @@ export class QueryBuilder<T extends BaseModel> {
// Query execution methods // Query execution methods
async exists(): Promise<boolean> { async exists(): Promise<boolean> {
// Mock implementation const results = await this.limit(1).exec();
return false; return results.length > 0;
} }
async count(): Promise<number> { async count(): Promise<number> {
// Mock implementation const executor = new QueryExecutor<T>(this.model, this);
return 0; return await executor.count();
} }
async sum(field: string): Promise<number> { async sum(field: string): Promise<number> {
// Mock implementation const executor = new QueryExecutor<T>(this.model, this);
return 0; return await executor.sum(field);
} }
async average(field: string): Promise<number> { async average(field: string): Promise<number> {
// Mock implementation const executor = new QueryExecutor<T>(this.model, this);
return 0; return await executor.avg(field);
} }
async min(field: string): Promise<number> { async min(field: string): Promise<number> {
// Mock implementation const executor = new QueryExecutor<T>(this.model, this);
return 0; return await executor.min(field);
} }
async max(field: string): Promise<number> { async max(field: string): Promise<number> {
// Mock implementation const executor = new QueryExecutor<T>(this.model, this);
return 0; return await executor.max(field);
} }
async find(): Promise<T[]> {
// Mock implementation
return [];
}
async findOne(): Promise<T | null> {
// Mock implementation
return null;
}
async exec(): Promise<T[]> {
// Mock implementation - same as find
return [];
}
async first(): Promise<T | null> {
// Mock implementation - same as findOne
return null;
}
async paginate(page: number, perPage: number): Promise<any> {
// Mock implementation
return {
data: [],
total: 0,
page,
perPage,
totalPages: 0,
hasMore: false
};
}
// Clone query for reuse // Clone query for reuse
clone(): QueryBuilder<T> { clone(): QueryBuilder<T> {

View File

@ -119,7 +119,9 @@ export class ShardManager {
const normalizedCode = Math.max(97, Math.min(122, charCode)); const normalizedCode = Math.max(97, Math.min(122, charCode));
const range = (normalizedCode - 97) / 25; // 0-1 range const range = (normalizedCode - 97) / 25; // 0-1 range
return Math.floor(range * shardCount); const shardIndex = Math.floor(range * shardCount);
// Ensure the index is within bounds (handle edge case where range = 1.0)
return Math.min(shardIndex, shardCount - 1);
} }
private userSharding(key: string, shardCount: number): number { private userSharding(key: string, shardCount: number): number {

View File

@ -460,8 +460,14 @@ describe('BaseModel', () => {
expect(user.validateEmail()).toBe(true); expect(user.validateEmail()).toBe(true);
user.email = 'invalid-email'; // Test that setting an invalid email throws validation error
expect(user.validateEmail()).toBe(false); expect(() => {
user.email = 'invalid-email';
}).toThrow('email failed custom validation');
// Email should still be the original valid value
expect(user.email).toBe('valid@example.com');
expect(user.validateEmail()).toBe(true);
}); });
}); });
}); });