UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

374 lines (311 loc) 11.2 kB
import "reflect-metadata"; import { IQueryBuilder, IQueryConditionBuilder, QueryCondition, QueryOperator, QueryResult, QuerySort, QueryInclude, AsyncConditionFunction, PropertyMetadata, RelationshipMetadata, CompiledQuery } from "./types"; /** * Core QueryBuilder class - implements progressive disclosure interface * for both humans and AI agents */ export class QueryBuilder<T> implements IQueryBuilder<T> { protected conditions: QueryCondition[] = []; protected asyncConditions: AsyncConditionFunction<T>[] = []; protected sorts: QuerySort[] = []; protected includes: QueryInclude[] = []; protected limitCount?: number; protected offsetCount?: number; private entityClass: new (...args: any[]) => T; constructor(entityClass: new (...args: any[]) => T) { this.entityClass = entityClass; } // Basic conditions where(property: keyof T): IQueryConditionBuilder<T> { return new QueryConditionBuilder(this.clone(), property as string); } // Logical operators and(property: keyof T): IQueryConditionBuilder<T> { return new QueryConditionBuilder(this.clone(), property as string, 'and'); } or(property: keyof T): IQueryConditionBuilder<T> { return new QueryConditionBuilder(this.clone(), property as string, 'or'); } not(property: keyof T): IQueryConditionBuilder<T> { return new QueryConditionBuilder(this.clone(), property as string, 'not'); } // Async conditions for AI agents whereAsync(condition: AsyncConditionFunction<T>): IQueryBuilder<T> { const cloned = this.clone(); cloned.asyncConditions.push(condition); return cloned; } andAsync(condition: AsyncConditionFunction<T>): IQueryBuilder<T> { const cloned = this.clone(); cloned.asyncConditions.push(condition); return cloned; } orAsync(condition: AsyncConditionFunction<T>): IQueryBuilder<T> { const cloned = this.clone(); cloned.asyncConditions.push(condition); return cloned; } // Relationships include(relationship: string): IQueryBuilder<T> { const cloned = this.clone(); cloned.includes.push({ relationship }); return cloned; } includeNested(relationship: string, nested: QueryInclude[]): IQueryBuilder<T> { const cloned = this.clone(); cloned.includes.push({ relationship, nested }); return cloned; } // Sorting & Pagination orderBy(property: keyof T, direction: 'asc' | 'desc' = 'asc'): IQueryBuilder<T> { const cloned = this.clone(); cloned.sorts.push({ property: property as string, direction }); return cloned; } limit(count: number): IQueryBuilder<T> { const cloned = this.clone(); cloned.limitCount = count; return cloned; } offset(count: number): IQueryBuilder<T> { const cloned = this.clone(); cloned.offsetCount = count; return cloned; } // Execution methods (to be implemented with adapters) async execute(): Promise<QueryResult<T>> { // This will be implemented with database adapters throw new Error("Execute method requires database adapter implementation"); } async first(): Promise<T | null> { const result = await this.limit(1).execute(); return result.data.length > 0 ? result.data[0] : null; } async count(): Promise<number> { const result = await this.execute(); return result.total; } async exists(): Promise<boolean> { const result = await this.limit(1).execute(); return result.data.length > 0; } // AI Agent Discovery Methods - THE MAGIC! 🎯 getAvailableProperties(): (keyof T)[] { const properties = Reflect.getMetadata("entity:properties", this.entityClass) || []; return properties as (keyof T)[]; } getAvailableRelationships(): string[] { return Reflect.getMetadata("entity:relationships", this.entityClass) || []; } getAvailableOperators(): QueryOperator[] { return [ 'equals', 'eq', 'not_equals', 'ne', 'not', 'greater_than', 'gt', 'greater_than_or_equal', 'gte', 'less_than', 'lt', 'less_than_or_equal', 'lte', 'in', 'not_in', 'like', 'ilike', 'contains', 'starts_with', 'ends_with', 'is_null', 'is_not_null', 'between', 'not_between', 'regex', 'not_regex' ]; } describeProperty(property: keyof T): PropertyMetadata { const propertyName = property as string; const options = Reflect.getMetadata("property:options", this.entityClass.prototype, propertyName) || {}; const validationRules = Reflect.getMetadata("validation:rules", this.entityClass.prototype, propertyName) || []; const isId = Reflect.getMetadata("property:id", this.entityClass.prototype, propertyName) || false; const isIndexed = Reflect.getMetadata("index:options", this.entityClass.prototype, propertyName) !== undefined; return { name: propertyName, type: options.type || 'unknown', required: options.required || isId, unique: options.unique || isId, indexed: isIndexed, description: options.description, validationRules, examples: this.generateExamples(propertyName, options.type) }; } describeRelationship(relationship: string): RelationshipMetadata { const relationshipType = Reflect.getMetadata("relationship:type", this.entityClass.prototype, relationship); const relationshipOptions = Reflect.getMetadata("relationship:options", this.entityClass.prototype, relationship) || {}; return { name: relationship, type: relationshipType || 'unknown', target: relationshipOptions.target?.name || 'unknown', inverse: relationshipOptions.inverse, required: relationshipOptions.required || false, description: relationshipOptions.description }; } // Internal helper methods addCondition(condition: QueryCondition): void { this.conditions.push(condition); } addAsyncCondition(condition: AsyncConditionFunction<T>): void { this.asyncConditions.push(condition); } getConditions(): QueryCondition[] { return [...this.conditions]; } getAsyncConditions(): AsyncConditionFunction<T>[] { return [...this.asyncConditions]; } getSorts(): QuerySort[] { return [...this.sorts]; } getIncludes(): QueryInclude[] { return [...this.includes]; } getLimit(): number | undefined { return this.limitCount; } getOffset(): number | undefined { return this.offsetCount; } // Clone method for immutability private clone(): QueryBuilder<T> { const cloned = new QueryBuilder(this.entityClass); cloned.conditions = [...this.conditions]; cloned.asyncConditions = [...this.asyncConditions]; cloned.sorts = [...this.sorts]; cloned.includes = [...this.includes]; cloned.limitCount = this.limitCount; cloned.offsetCount = this.offsetCount; return cloned; } private generateExamples(propertyName: string, type: string): any[] { // Generate contextual examples based on property name and type if (propertyName.includes('email')) return ['user@example.com', 'admin@company.org']; if (propertyName.includes('age')) return [25, 30, 45]; if (propertyName.includes('name')) return ['John Doe', 'Jane Smith']; if (propertyName.includes('status')) return ['active', 'inactive', 'pending']; if (type === 'number') return [1, 100, 500]; if (type === 'string') return ['example', 'test', 'sample']; if (type === 'boolean') return [true, false]; return []; } } /** * QueryConditionBuilder - handles the fluent condition building * This is where the progressive disclosure magic happens! ✨ */ export class QueryConditionBuilder<T> implements IQueryConditionBuilder<T> { private queryBuilder: QueryBuilder<T>; private property: string; private logicalOperator?: 'and' | 'or' | 'not'; constructor(queryBuilder: QueryBuilder<T>, property: string, logicalOperator?: 'and' | 'or' | 'not') { this.queryBuilder = queryBuilder; this.property = property; this.logicalOperator = logicalOperator; } // Comparison operators equals(value: any): IQueryBuilder<T> { return this.addCondition('equals', value); } eq(value: any): IQueryBuilder<T> { return this.addCondition('eq', value); } not(value: any): IQueryBuilder<T> { return this.addCondition('not', value); } ne(value: any): IQueryBuilder<T> { return this.addCondition('ne', value); } // Numeric operators gt(value: number): IQueryBuilder<T> { return this.addCondition('gt', value); } gte(value: number): IQueryBuilder<T> { return this.addCondition('gte', value); } lt(value: number): IQueryBuilder<T> { return this.addCondition('lt', value); } lte(value: number): IQueryBuilder<T> { return this.addCondition('lte', value); } between(min: number, max: number): IQueryBuilder<T> { return this.addCondition('between', { min, max }); } // Array operators in(values: any[]): IQueryBuilder<T> { return this.addCondition('in', values); } notIn(values: any[]): IQueryBuilder<T> { return this.addCondition('not_in', values); } // String operators like(pattern: string): IQueryBuilder<T> { return this.addCondition('like', pattern); } ilike(pattern: string): IQueryBuilder<T> { return this.addCondition('ilike', pattern); } contains(value: string): IQueryBuilder<T> { return this.addCondition('contains', value); } startsWith(value: string): IQueryBuilder<T> { return this.addCondition('starts_with', value); } endsWith(value: string): IQueryBuilder<T> { return this.addCondition('ends_with', value); } regex(pattern: RegExp): IQueryBuilder<T> { return this.addCondition('regex', pattern); } // Null operators isNull(): IQueryBuilder<T> { return this.addCondition('is_null', null); } isNotNull(): IQueryBuilder<T> { return this.addCondition('is_not_null', null); } // Async operators for AI agents equalsAsync(valueProvider: () => Promise<any>): IQueryBuilder<T> { return this.addAsyncCondition('equals', valueProvider); } matchesAsync(condition: AsyncConditionFunction<T>): IQueryBuilder<T> { // Use the public method to add async condition this.queryBuilder.addAsyncCondition(condition); return this.queryBuilder; } private addCondition(operator: QueryOperator, value: any): IQueryBuilder<T> { const condition: QueryCondition = { property: this.property, operator, value }; // QueryBuilder should already be cloned when passed to constructor this.queryBuilder.addCondition(condition); return this.queryBuilder; } private addAsyncCondition(operator: QueryOperator, valueProvider: () => Promise<any>): IQueryBuilder<T> { const condition: QueryCondition = { property: this.property, operator, value: valueProvider, async: true }; // QueryBuilder should already be cloned when passed to constructor this.queryBuilder.addCondition(condition); return this.queryBuilder; } } /** * Factory function to create a QueryBuilder for a specific entity */ export function createQueryBuilder<T>(entityClass: new (...args: any[]) => T): IQueryBuilder<T> { return new QueryBuilder(entityClass); }