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:
parent
9f425f2106
commit
0305cb1737
@ -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
|
||||||
|
if (typeof operatorOrValue === 'string') {
|
||||||
|
const lowerValue = operatorOrValue.toLowerCase();
|
||||||
|
if (lowerValue === 'is null' || lowerValue === 'is not null') {
|
||||||
|
finalOperator = lowerValue;
|
||||||
|
finalValue = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastCondition = this.conditions[this.conditions.length - 1];
|
|
||||||
if (lastCondition) {
|
|
||||||
lastCondition.logical = 'or';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
|
@ -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 {
|
||||||
|
@ -460,8 +460,14 @@ describe('BaseModel', () => {
|
|||||||
|
|
||||||
expect(user.validateEmail()).toBe(true);
|
expect(user.validateEmail()).toBe(true);
|
||||||
|
|
||||||
|
// Test that setting an invalid email throws validation error
|
||||||
|
expect(() => {
|
||||||
user.email = 'invalid-email';
|
user.email = 'invalid-email';
|
||||||
expect(user.validateEmail()).toBe(false);
|
}).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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
Reference in New Issue
Block a user