penguin-beta-version-1.5 #3

Merged
anonpenguin merged 30 commits from sotiris-beta-version-1.5 into main 2025-07-05 02:53:30 +00:00
3 changed files with 79 additions and 56 deletions
Showing only changes of commit 0305cb1737 - Show all commits

View File

@ -28,22 +28,55 @@ export class QueryBuilder<T extends BaseModel> {
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<T extends BaseModel> {
});
} 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<T extends BaseModel> {
return this;
}
// Execution methods
// Execution methods
async exec(): Promise<T[]> {
const executor = new QueryExecutor<T>(this.model, this);
return await executor.execute();
@ -258,6 +295,15 @@ export class QueryBuilder<T extends BaseModel> {
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> {
const results = await this.limit(1).exec();
return results[0] || null;
@ -513,66 +559,35 @@ export class QueryBuilder<T extends BaseModel> {
// Query execution methods
async exists(): Promise<boolean> {
// Mock implementation
return false;
const results = await this.limit(1).exec();
return results.length > 0;
}
async count(): Promise<number> {
// Mock implementation
return 0;
const executor = new QueryExecutor<T>(this.model, this);
return await executor.count();
}
async sum(field: string): Promise<number> {
// Mock implementation
return 0;
const executor = new QueryExecutor<T>(this.model, this);
return await executor.sum(field);
}
async average(field: string): Promise<number> {
// Mock implementation
return 0;
const executor = new QueryExecutor<T>(this.model, this);
return await executor.avg(field);
}
async min(field: string): Promise<number> {
// Mock implementation
return 0;
const executor = new QueryExecutor<T>(this.model, this);
return await executor.min(field);
}
async max(field: string): Promise<number> {
// Mock implementation
return 0;
const executor = new QueryExecutor<T>(this.model, this);
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(): QueryBuilder<T> {

View File

@ -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 {

View File

@ -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);
});
});
});