diff --git a/src/framework/query/QueryBuilder.ts b/src/framework/query/QueryBuilder.ts index f39b49c..474e4d1 100644 --- a/src/framework/query/QueryBuilder.ts +++ b/src/framework/query/QueryBuilder.ts @@ -28,22 +28,55 @@ export class QueryBuilder { where(field: string, operatorOrValue: string | any, value?: any): this { if (value !== undefined) { // 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 { // 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 }); } 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 { return this.where(field, 'in', values); } whereNotIn(field: string, values: any[]): this { - return this.where(field, 'not_in', values); + return this.where(field, 'not in', values); } + whereNull(field: string): this { this.conditions.push({ field, operator: 'is null', value: null }); return this; @@ -126,17 +159,21 @@ export class QueryBuilder { }); } else { // Simple orWhere version: orWhere('field', 'operator', 'value') or orWhere('field', 'value') - let finalOperator = '='; + let finalOperator = 'eq'; let finalValue = operatorOrValue; if (value !== undefined) { - finalOperator = operatorOrValue; + finalOperator = this.normalizeOperator(operatorOrValue); finalValue = value; - } - - const lastCondition = this.conditions[this.conditions.length - 1]; - if (lastCondition) { - lastCondition.logical = 'or'; + } 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; + } + } } this.conditions.push({ @@ -248,7 +285,7 @@ export class QueryBuilder { return this; } - // Execution methods + // Execution methods async exec(): Promise { const executor = new QueryExecutor(this.model, this); return await executor.execute(); @@ -258,6 +295,15 @@ export class QueryBuilder { return await this.exec(); } + async find(): Promise { + return await this.exec(); + } + + async findOne(): Promise { + const results = await this.limit(1).exec(); + return results[0] || null; + } + async first(): Promise { const results = await this.limit(1).exec(); return results[0] || null; @@ -513,66 +559,35 @@ export class QueryBuilder { // Query execution methods async exists(): Promise { - // Mock implementation - return false; + const results = await this.limit(1).exec(); + return results.length > 0; } async count(): Promise { - // Mock implementation - return 0; + const executor = new QueryExecutor(this.model, this); + return await executor.count(); } async sum(field: string): Promise { - // Mock implementation - return 0; + const executor = new QueryExecutor(this.model, this); + return await executor.sum(field); } async average(field: string): Promise { - // Mock implementation - return 0; + const executor = new QueryExecutor(this.model, this); + return await executor.avg(field); } async min(field: string): Promise { - // Mock implementation - return 0; + const executor = new QueryExecutor(this.model, this); + return await executor.min(field); } async max(field: string): Promise { - // Mock implementation - return 0; + const executor = new QueryExecutor(this.model, this); + return await executor.max(field); } - async find(): Promise { - // Mock implementation - return []; - } - - async findOne(): Promise { - // Mock implementation - return null; - } - - async exec(): Promise { - // Mock implementation - same as find - return []; - } - - async first(): Promise { - // Mock implementation - same as findOne - return null; - } - - async paginate(page: number, perPage: number): Promise { - // Mock implementation - return { - data: [], - total: 0, - page, - perPage, - totalPages: 0, - hasMore: false - }; - } // Clone query for reuse clone(): QueryBuilder { diff --git a/src/framework/sharding/ShardManager.ts b/src/framework/sharding/ShardManager.ts index 3ff2da0..6d3e1cc 100644 --- a/src/framework/sharding/ShardManager.ts +++ b/src/framework/sharding/ShardManager.ts @@ -119,7 +119,9 @@ export class ShardManager { const normalizedCode = Math.max(97, Math.min(122, charCode)); 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 { diff --git a/tests/unit/models/BaseModel.test.ts b/tests/unit/models/BaseModel.test.ts index 6b8baa0..a92ef27 100644 --- a/tests/unit/models/BaseModel.test.ts +++ b/tests/unit/models/BaseModel.test.ts @@ -460,8 +460,14 @@ describe('BaseModel', () => { expect(user.validateEmail()).toBe(true); - user.email = 'invalid-email'; - expect(user.validateEmail()).toBe(false); + // Test that setting an invalid email throws validation error + 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); }); }); }); \ No newline at end of file