|
// === HIGH-LEVEL DOMAIN TYPES === |
|
|
|
// Domain Entities |
|
export interface User { |
|
id: string; |
|
email: string; |
|
name: string; |
|
age: number; |
|
isActive: boolean; |
|
createdAt: Date; |
|
role: UserRole; |
|
tags: string[]; |
|
} |
|
|
|
export interface Product { |
|
id: string; |
|
name: string; |
|
price: number; |
|
category: string; |
|
inStock: boolean; |
|
tags: string[]; |
|
ratings: number[]; |
|
createdAt: Date; |
|
status: ProductStatus; |
|
} |
|
|
|
// Domain Value Types |
|
const userRole = ['admin', 'user', 'moderator'] as const; |
|
export type UserRole = (typeof userRole)[number]; |
|
|
|
const productStatus = ['draft', 'published', 'archived'] as const; |
|
export type ProductStatus = (typeof productStatus)[number]; |
|
|
|
// === FILTER QUERY INTERFACE === |
|
|
|
export interface FilterQuery<T> { |
|
filters: Filter<T>[]; |
|
limit?: number; |
|
offset?: number; |
|
sortBy?: keyof T; |
|
sortOrder?: 'asc' | 'desc'; |
|
} |
|
|
|
export interface Filter<T> { |
|
field: keyof T; |
|
operator: FilterOperator; |
|
value?: any; |
|
} |
|
|
|
// === API SERVICE INTERFACE === |
|
|
|
export interface ApiResponse<T> { |
|
success: boolean; |
|
data?: T; |
|
errors?: readonly string[]; |
|
} |
|
|
|
export interface ApiFilterService<T> { |
|
findEntities(query: FilterQuery<T>): Promise<ApiResponse<T[]>>; |
|
countEntities(filters: Filter<T>[]): Promise<ApiResponse<number>>; |
|
createEntity(data: Omit<T, 'id'>): Promise<ApiResponse<T>>; |
|
updateEntity(id: string, updates: Partial<T>): Promise<ApiResponse<T>>; |
|
deleteEntity(id: string): Promise<ApiResponse<void>>; |
|
} |
|
|
|
// === REPOSITORY INTERFACE === |
|
|
|
export interface Repository<T> { |
|
findByFilters(query: FilterQuery<T>): Promise<T[]>; |
|
count(filters: Filter<T>[]): Promise<number>; |
|
create(entity: Omit<T, 'id'>): Promise<T>; |
|
update(id: string, updates: Partial<T>): Promise<T>; |
|
delete(id: string): Promise<void>; |
|
} |
|
|
|
// === SERVICE FACTORIES === |
|
|
|
export function createUserService(userRepository: Repository<User>): ApiFilterService<User> { |
|
const validationService = createFilterValidationService(userFieldRegistry); |
|
return createApiFilterService(userRepository, validationService); |
|
} |
|
|
|
export function createProductService(productRepository: Repository<Product>): ApiFilterService<Product> { |
|
const validationService = createFilterValidationService(productFieldRegistry); |
|
return createApiFilterService(productRepository, validationService); |
|
} |
|
|
|
// === API SERVICE IMPLEMENTATION === |
|
|
|
export function createApiFilterService<T>( |
|
repository: Repository<T>, |
|
validationService: FilterValidationService<T> |
|
): ApiFilterService<T> { |
|
|
|
const findEntities = async (query: FilterQuery<T>): Promise<ApiResponse<T[]>> => { |
|
const validation = validationService.validateQuery(query); |
|
|
|
if (!validation.isValid) { |
|
return { |
|
success: false, |
|
errors: validation.errors |
|
}; |
|
} |
|
|
|
try { |
|
const entities = await repository.findByFilters(query); |
|
return { |
|
success: true, |
|
data: entities |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
errors: ['Internal server error'] |
|
}; |
|
} |
|
}; |
|
|
|
const countEntities = async (filters: Filter<T>[]): Promise<ApiResponse<number>> => { |
|
const query: FilterQuery<T> = { filters }; |
|
const validation = validationService.validateQuery(query); |
|
|
|
if (!validation.isValid) { |
|
return { |
|
success: false, |
|
errors: validation.errors |
|
}; |
|
} |
|
|
|
try { |
|
const count = await repository.count(filters); |
|
return { |
|
success: true, |
|
data: count |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
errors: ['Internal server error'] |
|
}; |
|
} |
|
}; |
|
|
|
const createEntity = async (data: Omit<T, 'id'>): Promise<ApiResponse<T>> => { |
|
try { |
|
const entity = await repository.create(data); |
|
return { |
|
success: true, |
|
data: entity |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
errors: ['Failed to create entity'] |
|
}; |
|
} |
|
}; |
|
|
|
const updateEntity = async (id: string, updates: Partial<T>): Promise<ApiResponse<T>> => { |
|
try { |
|
const entity = await repository.update(id, updates); |
|
return { |
|
success: true, |
|
data: entity |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
errors: ['Failed to update entity'] |
|
}; |
|
} |
|
}; |
|
|
|
const deleteEntity = async (id: string): Promise<ApiResponse<void>> => { |
|
try { |
|
await repository.delete(id); |
|
return { |
|
success: true |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
errors: ['Failed to delete entity'] |
|
}; |
|
} |
|
}; |
|
|
|
return { |
|
findEntities, |
|
countEntities, |
|
createEntity, |
|
updateEntity, |
|
deleteEntity |
|
}; |
|
} |
|
|
|
// === VALIDATION SERVICE === |
|
|
|
export interface ValidationResult { |
|
isValid: boolean; |
|
errors: readonly string[]; |
|
} |
|
|
|
export interface FilterValidationService<T> { |
|
validateQuery(query: FilterQuery<T>): ValidationResult; |
|
} |
|
|
|
export function createFilterValidationService<T>( |
|
fieldRegistry: FieldEvaluatorRegistry<T> |
|
): FilterValidationService<T> { |
|
|
|
const validateQuery = (query: FilterQuery<T>): ValidationResult => { |
|
const errors: string[] = []; |
|
|
|
for (const filter of query.filters) { |
|
if (!fieldRegistry.validateFilter(filter)) { |
|
const metadata = fieldRegistry.getMetadata(filter.field); |
|
if (!metadata) { |
|
errors.push(`Unknown field: ${String(filter.field)}`); |
|
} else { |
|
const allowedOps = fieldRegistry.getAllowedOperators(filter.field); |
|
errors.push(`Invalid operator ${filter.operator} for field ${String(filter.field)}. Allowed: ${allowedOps.join(', ')}`); |
|
} |
|
} |
|
|
|
const validationError = validateFilterValue(filter); |
|
if (validationError) { |
|
errors.push(validationError); |
|
} |
|
} |
|
|
|
return { |
|
isValid: errors.length === 0, |
|
errors |
|
}; |
|
}; |
|
|
|
const validateFilterValue = (filter: Filter<T>): string | null => { |
|
const metadata = fieldRegistry.getMetadata(filter.field); |
|
if (!metadata) return null; |
|
|
|
const noValueOperators: readonly FilterOperator[] = [ |
|
'is_null', |
|
'is_not_null', |
|
'array_empty', |
|
'array_not_empty' |
|
]; |
|
|
|
if (noValueOperators.includes(filter.operator)) { |
|
return null; |
|
} |
|
|
|
if (filter.value === undefined || filter.value === null) { |
|
return `Value required for operator ${filter.operator}`; |
|
} |
|
|
|
switch (metadata.type) { |
|
case 'literal_union': |
|
return validateLiteralUnionValue(filter, metadata); |
|
case 'number': |
|
return validateNumberValue(filter); |
|
case 'boolean': |
|
return validateBooleanValue(filter); |
|
case 'date': |
|
return validateDateValue(filter); |
|
case 'string_array': |
|
return validateArrayValue(filter, 'string'); |
|
case 'number_array': |
|
return validateArrayValue(filter, 'number'); |
|
default: |
|
return null; |
|
} |
|
}; |
|
|
|
const validateLiteralUnionValue = (filter: Filter<T>, metadata: FieldMetadata): string | null => { |
|
if (metadata.allowedValues) { |
|
const inOperators: readonly FilterOperator[] = ['in', 'not_in']; |
|
|
|
if (inOperators.includes(filter.operator)) { |
|
if (!Array.isArray(filter.value)) { |
|
return `Expected array for ${filter.operator} operator`; |
|
} |
|
const invalidValues = filter.value.filter(v => !metadata.allowedValues!.includes(v)); |
|
if (invalidValues.length > 0) { |
|
return `Invalid values: ${invalidValues.join(', ')}. Allowed: ${metadata.allowedValues.join(', ')}`; |
|
} |
|
} else if (!metadata.allowedValues.includes(filter.value)) { |
|
return `Invalid value: ${filter.value}. Allowed: ${metadata.allowedValues.join(', ')}`; |
|
} |
|
} |
|
return null; |
|
}; |
|
|
|
const validateNumberValue = (filter: Filter<T>): string | null => { |
|
const arrayOperators: readonly FilterOperator[] = ['in', 'not_in']; |
|
|
|
if (arrayOperators.includes(filter.operator)) { |
|
if (!Array.isArray(filter.value) || !filter.value.every(v => typeof v === 'number')) { |
|
return `Expected array of numbers for ${filter.operator} operator`; |
|
} |
|
} else if (typeof filter.value !== 'number') { |
|
return `Expected number for field ${String(filter.field)}`; |
|
} |
|
return null; |
|
}; |
|
|
|
const validateBooleanValue = (filter: Filter<T>): string | null => { |
|
if (typeof filter.value !== 'boolean') { |
|
return `Expected boolean for field ${String(filter.field)}`; |
|
} |
|
return null; |
|
}; |
|
|
|
const validateDateValue = (filter: Filter<T>): string | null => { |
|
if (!(filter.value instanceof Date) && typeof filter.value !== 'string') { |
|
return `Expected Date or ISO string for field ${String(filter.field)}`; |
|
} |
|
return null; |
|
}; |
|
|
|
const validateArrayValue = (filter: Filter<T>, elementType: 'string' | 'number'): string | null => { |
|
const typeCheck = elementType === 'string' |
|
? (v: any) => typeof v === 'string' |
|
: (v: any) => typeof v === 'number'; |
|
|
|
const lengthOperators: readonly FilterOperator[] = [ |
|
'array_length_eq', |
|
'array_length_gt', |
|
'array_length_gte', |
|
'array_length_lt', |
|
'array_length_lte' |
|
]; |
|
|
|
const singleValueOperators: readonly FilterOperator[] = ['array_contains']; |
|
const multiValueOperators: readonly FilterOperator[] = ['array_contains_any', 'array_contains_all']; |
|
|
|
if (lengthOperators.includes(filter.operator)) { |
|
if (typeof filter.value !== 'number') { |
|
return `Expected number for ${filter.operator}`; |
|
} |
|
} else if (singleValueOperators.includes(filter.operator)) { |
|
if (!typeCheck(filter.value)) { |
|
return `Expected ${elementType} value for ${filter.operator}`; |
|
} |
|
} else if (multiValueOperators.includes(filter.operator)) { |
|
if (!Array.isArray(filter.value) || !filter.value.every(typeCheck)) { |
|
return `Expected array of ${elementType}s for ${filter.operator}`; |
|
} |
|
} |
|
return null; |
|
}; |
|
|
|
return { validateQuery }; |
|
} |
|
|
|
// === FIELD REGISTRY === |
|
|
|
export interface FieldEvaluatorRegistry<T> { |
|
getMetadata(field: keyof T): FieldMetadata | undefined; |
|
getAllowedOperators(field: keyof T): readonly FilterOperator[]; |
|
validateFilter(filter: Filter<T>): boolean; |
|
getAllFields(): readonly (keyof T)[]; |
|
getFieldsByType(type: FieldType): readonly (keyof T)[]; |
|
} |
|
|
|
export function createFieldEvaluatorRegistry<T>(): { |
|
register: (field: keyof T, metadata: FieldMetadata) => void; |
|
registry: FieldEvaluatorRegistry<T>; |
|
} { |
|
const fieldMappings = new Map<keyof T, FieldMetadata>(); |
|
|
|
const register = (field: keyof T, metadata: FieldMetadata): void => { |
|
fieldMappings.set(field, metadata); |
|
}; |
|
|
|
const getMetadata = (field: keyof T): FieldMetadata | undefined => { |
|
return fieldMappings.get(field); |
|
}; |
|
|
|
const getAllowedOperators = (field: keyof T): readonly FilterOperator[] => { |
|
const metadata = getMetadata(field); |
|
if (!metadata) return []; |
|
|
|
return FIELD_TYPE_OPERATORS[metadata.type].allowedOperators; |
|
}; |
|
|
|
const validateFilter = (filter: Filter<T>): boolean => { |
|
const allowedOperators = getAllowedOperators(filter.field); |
|
return allowedOperators.includes(filter.operator); |
|
}; |
|
|
|
const getAllFields = (): readonly (keyof T)[] => { |
|
return Array.from(fieldMappings.keys()); |
|
}; |
|
|
|
const getFieldsByType = (type: FieldType): readonly (keyof T)[] => { |
|
return Array.from(fieldMappings.entries()) |
|
.filter(([_, metadata]) => metadata.type === type) |
|
.map(([field, _]) => field); |
|
}; |
|
|
|
const registry: FieldEvaluatorRegistry<T> = { |
|
getMetadata, |
|
getAllowedOperators, |
|
validateFilter, |
|
getAllFields, |
|
getFieldsByType |
|
}; |
|
|
|
return { register, registry }; |
|
} |
|
|
|
// === FIELD REGISTRIES CONFIGURATION === |
|
|
|
// User Field Registry |
|
const userFieldEvaluator = createFieldEvaluatorRegistry<User>(); |
|
|
|
userFieldEvaluator.register('id', { type: 'string' }); |
|
userFieldEvaluator.register('email', { type: 'string' }); |
|
userFieldEvaluator.register('name', { type: 'string' }); |
|
userFieldEvaluator.register('age', { type: 'number' }); |
|
userFieldEvaluator.register('isActive', { type: 'boolean' }); |
|
userFieldEvaluator.register('createdAt', { type: 'date' }); |
|
userFieldEvaluator.register('role', { type: 'literal_union', allowedValues: userRole }); |
|
userFieldEvaluator.register('tags', { type: 'string_array' }); |
|
|
|
export const userFieldRegistry = userFieldEvaluator.registry; |
|
|
|
// Product Field Registry |
|
const productFieldEvaluator = createFieldEvaluatorRegistry<Product>(); |
|
|
|
productFieldEvaluator.register('id', { type: 'string' }); |
|
productFieldEvaluator.register('name', { type: 'string' }); |
|
productFieldEvaluator.register('price', { type: 'number' }); |
|
productFieldEvaluator.register('category', { type: 'string' }); |
|
productFieldEvaluator.register('inStock', { type: 'boolean' }); |
|
productFieldEvaluator.register('tags', { type: 'string_array' }); |
|
productFieldEvaluator.register('ratings', { type: 'number_array' }); |
|
productFieldEvaluator.register('createdAt', { type: 'date' }); |
|
productFieldEvaluator.register('status', { type: 'literal_union', allowedValues: productStatus }); |
|
|
|
export const productFieldRegistry = productFieldEvaluator.registry; |
|
|
|
// === FIELD TYPE SYSTEM === |
|
|
|
export interface FieldMetadata { |
|
type: FieldType; |
|
allowedValues?: readonly string[]; |
|
} |
|
|
|
export interface FieldTypeConfig { |
|
allowedOperators: readonly FilterOperator[]; |
|
} |
|
|
|
const fieldType = [ |
|
'string', |
|
'number', |
|
'boolean', |
|
'date', |
|
'literal_union', |
|
'string_array', |
|
'number_array' |
|
] as const; |
|
export type FieldType = (typeof fieldType)[number]; |
|
|
|
const filterOperator = [ |
|
'eq', |
|
'ne', |
|
'gt', |
|
'gte', |
|
'lt', |
|
'lte', |
|
'contains', |
|
'starts_with', |
|
'ends_with', |
|
'in', |
|
'not_in', |
|
'is_null', |
|
'is_not_null', |
|
'array_contains', |
|
'array_contains_any', |
|
'array_contains_all', |
|
'array_length_eq', |
|
'array_length_gt', |
|
'array_length_gte', |
|
'array_length_lt', |
|
'array_length_lte', |
|
'array_empty', |
|
'array_not_empty' |
|
] as const; |
|
export type FilterOperator = (typeof filterOperator)[number]; |
|
|
|
// === OPERATOR MAPPINGS === |
|
|
|
const stringOperators = [ |
|
'eq', |
|
'ne', |
|
'contains', |
|
'starts_with', |
|
'ends_with', |
|
'in', |
|
'not_in', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
const numberOperators = [ |
|
'eq', |
|
'ne', |
|
'gt', |
|
'gte', |
|
'lt', |
|
'lte', |
|
'in', |
|
'not_in', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
const booleanOperators = [ |
|
'eq', |
|
'ne', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
const dateOperators = [ |
|
'eq', |
|
'ne', |
|
'gt', |
|
'gte', |
|
'lt', |
|
'lte', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
const literalUnionOperators = [ |
|
'eq', |
|
'ne', |
|
'in', |
|
'not_in', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
const arrayOperators = [ |
|
'array_contains', |
|
'array_contains_any', |
|
'array_contains_all', |
|
'array_length_eq', |
|
'array_length_gt', |
|
'array_length_gte', |
|
'array_length_lt', |
|
'array_length_lte', |
|
'array_empty', |
|
'array_not_empty', |
|
'is_null', |
|
'is_not_null' |
|
] as const; |
|
|
|
export const FIELD_TYPE_OPERATORS: Record<FieldType, FieldTypeConfig> = { |
|
string: { allowedOperators: stringOperators }, |
|
number: { allowedOperators: numberOperators }, |
|
boolean: { allowedOperators: booleanOperators }, |
|
date: { allowedOperators: dateOperators }, |
|
literal_union: { allowedOperators: literalUnionOperators }, |
|
string_array: { allowedOperators: arrayOperators }, |
|
number_array: { allowedOperators: arrayOperators } |
|
}; |
|
|
|
// === USAGE EXAMPLES === |
|
|
|
const exampleUserQuery: FilterQuery<User> = { |
|
filters: [ |
|
{ field: 'isActive', operator: 'eq', value: true }, |
|
{ field: 'age', operator: 'gt', value: 18 }, |
|
{ field: 'role', operator: 'in', value: ['user', 'moderator'] }, |
|
{ field: 'tags', operator: 'array_contains', value: 'premium' } |
|
], |
|
limit: 10, |
|
offset: 0, |
|
sortBy: 'createdAt', |
|
sortOrder: 'desc' |
|
}; |
|
|
|
const exampleProductQuery: FilterQuery<Product> = { |
|
filters: [ |
|
{ field: 'inStock', operator: 'eq', value: true }, |
|
{ field: 'price', operator: 'lte', value: 100 }, |
|
{ field: 'status', operator: 'eq', value: 'published' }, |
|
{ field: 'tags', operator: 'array_contains_any', value: ['sale', 'featured'] }, |
|
{ field: 'ratings', operator: 'array_length_gte', value: 5 } |
|
], |
|
sortBy: 'price', |
|
sortOrder: 'asc' |
|
}; |