Query Params Integration
Criteria can be constructed directly from URL query parameters, making it perfect for REST APIs.
Basic Usage
import { Criteria } from "@woltz/rich-domain" ;
// Express.js example
app . get ( "/users" , ( req , res ) => {
const criteria = Criteria . fromQueryParams < User >( req . query );
const users = await userRepository . find ( criteria );
res . json ( users . toJSON ());
});
Filters follow the pattern: field:operator=value, nested under the filters key:
const criteria = Criteria . fromQueryParams < User >({
filters: {
"name:contains" : "john" ,
"age:greaterThan" : "18" ,
"status:equals" : "active" ,
},
});
Supported Parameters
Parameter Key in QueryParamsObject Type Example Filters filtersRecord<string, unknown>{ "age:greaterThan": "18" }Ordering orderBystring[]["createdAt:desc"]Multiple orders orderBystring[]["featured:desc", "price:asc"]Pagination pagination{ page: number, limit: number }{ page: 2, limit: 20 }Search searchstring"laptop"
Complete Example
const criteria = Criteria . fromQueryParams < Product >({
filters: {
"status:equals" : "active" ,
"price:lessThanOrEqual" : "100" ,
"category:in" : [ "electronics" , "accessories" ],
},
search: "wireless" ,
orderBy: [ "featured:desc" , "price:asc" ],
pagination: { page: 1 , limit: 24 },
});
Value Parsing
Values are automatically parsed to their correct types:
// Numbers
"age:greaterThan" : "25" → 25 ( number )
// Booleans
"active:equals" : "true" → true ( boolean )
"active:equals" : "false" → false ( boolean )
// Dates (ISO format)
"createdAt:greaterThan" : "2024-01-01T00:00:00Z" → Date object
// Arrays (comma-separated)
"status:in" : "active,pending,draft" → [ "active" , "pending" , "draft" ]
// Between (two values)
"price:between" : "10,100" → [ 10 , 100 ]
Nested Fields in Query Params
const criteria = Criteria . fromQueryParams < User >({
filters: {
"profile.location.city:equals" : "New York" ,
"settings.theme:equals" : "dark" ,
},
});
Quantifiers in Query Params
Use @quantifier suffix for array field quantifiers:
const criteria = Criteria . fromQueryParams < User >({
filters: {
"posts.views:greaterThan@some" : "1000" , // Any post with > 1000 views
"posts.published:equals@every" : "true" , // All posts published
"comments.flagged:equals@none" : "true" , // No flagged comments
},
});
Field Adapters
Adapters map domain field names to database column names, allowing you to maintain clean API contracts while using different database schemas.
Why Use Adapters?
// Your domain model uses camelCase
interface UserDto {
id : string ;
firstName : string ;
lastName : string ;
emailAddress : string ;
createdAt : Date ;
}
// But your database uses snake_case
// users table: id, first_name, last_name, email_address, created_at
Creating an Adapter
import { CriteriaAdapter } from "@woltz/rich-domain" ;
const UserAdapter : CriteriaAdapter < UserDto , UserInDatabase > = {
firstName: "first_name" ,
lastName: "last_name" ,
emailAddress: "email_address" ,
createdAt: "created_at" ,
};
Using an Adapter
// Method 1: With useAdapter()
const criteria = Criteria . create < UserDto >()
. useAdapter ( UserAdapter )
. whereEquals ( "firstName" , "John" ) // Becomes: first_name = 'John'
. orderByDesc ( "createdAt" ); // Becomes: ORDER BY created_at DESC
// Method 2: With fromQueryParams()
const criteria = Criteria . fromQueryParams < UserDto >(
{ "firstName:equals" : "John" },
UserAdapter
);
// Method 3: With fromObject()
const criteria = Criteria . fromObject < UserDto >(
{ filters: [{ field: "firstName" , operator: "equals" , value: "John" }] },
UserAdapter
);
Nested Field Adapters
Map nested paths to different structures:
interface OrderDto {
id : string ;
customer : {
name : string ;
email : string ;
};
items : {
productName : string ;
quantity : number ;
}[];
}
interface OrderInDb {
id : string ;
customer_name : string ;
customer_email : string ;
order_items : {
product_name : string ;
qty : number ;
}[];
}
const OrderAdapter : CriteriaAdapter < OrderDto , OrderInDb > = {
"customer.name" : "customer_name" ,
"customer.email" : "customer_email" ,
items: "order_items" ,
"items.productName" : "order_items.product_name" ,
"items.quantity" : "order_items.qty" ,
};
const criteria = Criteria . create < OrderDto >()
. useAdapter ( OrderAdapter )
. whereContains ( "customer.name" , "John" ) // → customer_name LIKE '%John%'
. where ( "items.quantity" , "greaterThan" , 5 ); // → order_items.qty > 5
Adapter with Prefix Matching
Adapters support prefix matching for nested paths:
const adapter : CriteriaAdapter < Source , Target > = {
user: "app_user" , // Maps all user.* paths
};
// user.name → app_user.name
// user.profile.bio → app_user.profile.bio
Getting the Adapter
const criteria = Criteria . create < User >(). useAdapter ( UserAdapter );
const adapter = criteria . getAdapter (); // Returns the adapter or undefined
Serialization
toJSON()
Convert criteria to a plain object:
const criteria = Criteria . create < User >()
. whereEquals ( "status" , "active" )
. where ( "age" , "greaterThan" , 18 )
. orderByDesc ( "createdAt" )
. search ( "john" )
. paginate ( 2 , 20 );
const json = criteria . toJSON ();
Result:
{
"filters" : [
{ "field" : "status" , "operator" : "equals" , "value" : "active" },
{ "field" : "age" , "operator" : "greaterThan" , "value" : 18 }
],
"orders" : [{ "field" : "createdAt" , "direction" : "desc" }],
"pagination" : {
"page" : 2 ,
"limit" : 20 ,
"offset" : 20
},
"search" : 'john'
}
toQueryObject()
Convert criteria to a QueryParamsObject — useful when you need the structured object before converting to URL params:
const criteria = Criteria . create < User >()
. whereEquals ( "status" , "active" )
. where ( "age" , "greaterThan" , 18 )
. orderByDesc ( "createdAt" )
. paginate ( 2 , 20 );
const obj = criteria . toQueryObject ();
// {
// filters: { "status:equals": "active", "age:greaterThan": "18" },
// orders: [{ field: "createdAt", direction: "desc" }], ← resolved via adapter
// orderBy: ["createdAt:desc"],
// pagination: { page: 2, limit: 20, offset: 20 },
// }
toQueryParams()
Convert criteria directly to URLSearchParams for use in HTTP requests:
const criteria = Criteria . create < User >()
. whereEquals ( "status" , "active" )
. orderByDesc ( "createdAt" )
. paginate ( 1 , 20 );
const params = criteria . toQueryParams ();
// URLSearchParams {
// filters → '{"status:equals":"active"}',
// orderBy → '["createdAt:desc"]',
// page → "1",
// limit → "20",
// }
// Use in fetch
const response = await fetch ( `/api/users? ${ params . toString () } ` );
toQueryParams() and fromQueryParams() use the QueryParamsObject structure (with a filters key). When integrating with an HTTP server, parse the filters query param as JSON before passing it to fromQueryParams.
fromObject()
Reconstruct criteria from a plain object:
const criteria = Criteria . fromObject < User >({
filters: [
{ field: "status" , operator: "equals" , value: "active" },
{ field: "age" , operator: "greaterThan" , value: 18 },
],
orders: [{ field: "createdAt" , direction: "desc" }],
pagination: { page: 2 , limit: 20 , offset: 20 },
search: "john" ,
});
Use Cases for Serialization
API Transport Send criteria from frontend to backend
Caching Cache query configurations
Saved Filters Store user’s saved filter presets
Logging Log query configurations for debugging
Cloning
Create independent copies for query variations:
const baseCriteria = Criteria . create < User >()
. whereEquals ( "status" , "active" )
. whereNotNull ( "email" )
. orderByDesc ( "createdAt" );
// Clone for different use cases
const adminUsers = baseCriteria
. clone ()
. whereEquals ( "role" , "admin" )
. paginate ( 1 , 10 );
const recentUsers = baseCriteria
. clone ()
. where ( "createdAt" , "greaterThan" , lastWeek )
. paginate ( 1 , 50 );
const searchResults = baseCriteria
. clone ()
. search ( searchQuery )
. limit ( 20 );
// Original is unchanged
console . log ( baseCriteria . getFilters (). length ); // 2 (status, email)
Error Handling
Invalid Operator
try {
const criteria = Criteria . create < User >(). where ( "age" , "contains" , 18 ); // contains not valid for number
} catch ( error ) {
// InvalidCriteriaError: Operator "contains" is not valid for type "number".
// Valid operators: equals, notEquals, greaterThan, ...
}
Invalid Quantifier
try {
const criteria = Criteria . fromQueryParams < User >({
"posts.title:contains@invalid" : "test" ,
});
} catch ( error ) {
// InvalidCriteriaError: Invalid quantifier. Valid values: some, every, none
}
Express.js Integration Example
import express from "express" ;
import { Criteria , PaginatedResult } from "@woltz/rich-domain" ;
const app = express ();
// Middleware to parse criteria from query params
function parseCriteria < T >( adapter ?: CriteriaAdapter < any , any >) {
return (
req : express . Request ,
res : express . Response ,
next : express . NextFunction
) => {
try {
req . criteria = Criteria . fromQueryParams < T >( req . query , adapter );
next ();
} catch ( error ) {
res . status ( 400 ). json ({ error: error . message });
}
};
}
// Usage
app . get ( "/users" , parseCriteria < User >( UserAdapter ), async ( req , res ) => {
const criteria = req . criteria ;
const result = await userRepository . find ( criteria );
res . json ( result . toJSON ());
});
app . get (
"/products" ,
parseCriteria < Product >( ProductAdapter ),
async ( req , res ) => {
const criteria = req . criteria
. whereEquals ( "published" , true ) // Add server-side filter
. whereNull ( "deletedAt" );
const result = await productRepository . find ( criteria );
res . json ( result . toJSON ());
}
);
Fastify Integration Example
import Fastify from "fastify" ;
import { Criteria } from "@woltz/rich-domain" ;
const fastify = Fastify ();
fastify . get ( "/users" , async ( request , reply ) => {
const criteria = Criteria . fromQueryParams < User >(
request . query as Record < string , string >,
UserAdapter
);
const result = await userRepository . find ( criteria );
return result . toJSON ();
});
Frontend Integration Example
// React hook for building query strings
function useCriteriaQueryString < T >( criteria : Criteria < T >) : string {
const json = criteria . toJSON ();
const params = new URLSearchParams ();
// Add filters
json . filters . forEach (( filter ) => {
const key = ` ${ filter . field } : ${ filter . operator } ` ;
const value = Array . isArray ( filter . value )
? filter . value . join ( "," )
: String ( filter . value );
params . set ( key , value );
});
// Add ordering
if ( json . orders . length > 0 ) {
params . set (
"orderBy" ,
json . orders . map (( o ) => ` ${ o . field } : ${ o . direction } ` ). join ( "," )
);
}
// Add pagination
if ( json . pagination ) {
params . set ( "page" , String ( json . pagination . page ));
params . set ( "limit" , String ( json . pagination . limit ));
}
// Add search
if ( json . search ) {
params . set ( "search" , json . search );
}
return params . toString ();
}
// Usage
const criteria = Criteria . create < Product >()
. whereEquals ( "category" , "electronics" )
. orderByAsc ( "price" )
. paginate ( 1 , 20 );
const queryString = useCriteriaQueryString ( criteria );
// "category:equals=electronics&orderBy=price:asc&page=1&limit=20"
const response = await fetch ( `/api/products? ${ queryString } ` );
CriteriaAdapter Type Reference
type CriteriaAdapter < Input , Output > = {
[ K in FieldPath < Input >] ?: FieldPath < Output >;
};
// Example
interface SourceType {
userName : string ;
userEmail : string ;
profile : {
avatar : string ;
};
}
interface TargetType {
user_name : string ;
user_email : string ;
user_profile : {
avatar_url : string ;
};
}
const adapter : CriteriaAdapter < SourceType , TargetType > = {
userName: "user_name" ,
userEmail: "user_email" ,
profile: "user_profile" ,
"profile.avatar" : "user_profile.avatar_url" ,
};