@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
374 lines (311 loc) • 11.2 kB
text/typescript
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);
}