@hf-chimera/query-builder
v0.2.3
Published
Cross-end reactivity API - Query builder
Maintainers
Readme
ChimeraQueryBuilder
A type-safe query builder for constructing filters and ordering rules for Chimera Store collections. ChimeraQueryBuilder provides a fluent API for building complex queries with support for filtering, ordering, and nested grouping.
Features
- Type-Safe: Full TypeScript support with automatic type inference
- Fluent API: Chainable methods for building queries
- Complex Filtering: Support for operators, negations, and grouped conditions
- Ordering: Flexible sorting with null handling options
- Nested Groups: Create complex logical groupings with AND/OR/NOT
Installation
# Install both the core store and query builder
npm install @hf-chimera/store @hf-chimera/query-builder
# Or with yarn
yarn add @hf-chimera/store @hf-chimera/query-builder
# Or with pnpm
pnpm add @hf-chimera/store @hf-chimera/query-builderSetting Up Your Store
Before using ChimeraQueryBuilder, you need to create entity stores. Here's a complete example:
import { createChimeraEntityStore } from "@hf-chimera/store";
// Define your entity types
type Customer = {
id: number;
name: string;
email: string;
phone: string;
createdAt: Date;
};
type Order = {
id: number;
customerId: number;
productName: string;
quantity: number;
totalAmount: number;
status: "pending" | "completed" | "cancelled";
createdAt: Date;
};
// Create entity stores
export const customerStore = createChimeraEntityStore<"customer", Customer>({
name: "customer",
idGetter: "id",
async collectionFetcher(params, requestParams) {
const response = await fetch(`/api/customers`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filter: params.filter, order: params.order }),
signal: requestParams.signal,
});
return { data: await response.json() };
},
async itemCreator(item, requestParams) {
const response = await fetch(`/api/customers`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item),
signal: requestParams.signal,
});
return { data: await response.json() };
},
async itemUpdater(item, requestParams) {
const response = await fetch(`/api/customers/${item.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(item),
signal: requestParams.signal,
});
return { data: await response.json() };
},
async itemDeleter(id, requestParams) {
await fetch(`/api/customers/${id}`, {
method: "DELETE",
signal: requestParams.signal,
});
return { result: { id, success: true } };
},
async itemFetcher(params, requestParams) {
const response = await fetch(`/api/customers/${params.id}`, {
signal: requestParams.signal,
});
return { data: await response.json() };
},
});
export const orderStore = createChimeraEntityStore<"order", Order>({
name: "order",
idGetter: "id",
// ... similar CRUD operations
});Basic Usage
import { ChimeraQueryBuilder } from "@hf-chimera/query-builder";
import { customerStore } from "./store";
// Create a query builder instance
const query = new ChimeraQueryBuilder<typeof customerStore, Customer, any>();
// Build a simple query
query
.where("name", "eq", "John")
.where("age", "gte", 18)
.orderBy("createdAt", true); // true = descending
// Build the query descriptor
const descriptor = query.build();
// Returns: { filter: {...}, order: [...] }
// Use with entity store
const customers = customerStore.getCollection(descriptor);API Reference
orderBy(key, desc?, nulls?)
Adds an ordering rule to the query.
Parameters:
key:ChimeraPropertyGetter<Entity> | (keyof Entity & string)- The property to order by (can be a property name string or a property getter function)desc:boolean(default:false) - Whether to sort in descending ordernulls:ChimeraOrderNulls(default:ChimeraOrderNulls.Last) - How to handle null values (LastorFirst)
Returns: this - Returns the builder instance for chaining
Example:
query
.orderBy("name") // Ascending
.orderBy("age", true) // Descending
.orderBy("createdAt", false, ChimeraOrderNulls.First); // Nulls firstwhere(value, op, test)
Adds a filter condition to the query. Multiple where calls are combined with AND logic by default.
Parameters:
value:ChimeraPropertyGetter<Entity> | (KeysOfType<Entity, T> & string)- The property to filter on (property name or getter function)op:Op extends keyof OperatorsMap & string- The operator to use (e.g.,'eq','gte','in', etc.)test:Parameters<OperatorsMap[Op]>[1]- The value to compare against (type depends on the operator)
Returns: this - Returns the builder instance for chaining
Example:
query
.where("status", "eq", "active")
.where("age", "gte", 18)
.where("tags", "in", ["javascript", "typescript"]);whereNot(value, op, test)
Adds a negated filter condition (NOT condition) to the query.
Parameters:
value: Same aswhere()op: Same aswhere()test: Same aswhere()
Returns: this - Returns the builder instance for chaining
Example:
query.where("status", "eq", "active").whereNot("deleted", "eq", true); // NOT deleted = truegroup(conjunction, builder)
Creates a grouped condition with a specified logical conjunction. Useful for creating complex logical expressions with AND, OR, or NOT groups.
Parameters:
conjunction:ChimeraConjunctionType- The logical operator:'and','or', or'not'builder:QueryBuilderCreator<Store, Entity, OperatorsMap>- A callback function that receives a new builder instance for building the nested query
Returns: this - Returns the builder instance for chaining
Example:
// Create an OR group: (status = 'active' OR status = 'pending')
query.group("or", (q) => {
q.where("status", "eq", "active").where("status", "eq", "pending");
});
// Create a NOT group: NOT (age < 18 OR deleted = true)
query.group("not", (q) => {
q.group("or", (nested) => {
nested.where("age", "lt", 18).where("deleted", "eq", true);
});
});build()
Builds and returns the final query descriptor object.
Returns:
{
filter: ChimeraFilterDescriptor<OperatorsMap, Entity> | null;
order: (ChimeraOrderDescriptor < Entity > []) | null;
}filter: The filter descriptor, ornullif no filters were addedorder: An array of order descriptors, ornullif no ordering was specified
Example:
const { filter, order } = query.build();
// Use with store
const collection = store.from("customer").getCollection({ filter, order });Examples
Note: The following examples use generic type parameters like Store, User, and OperatorsMap. See Setting Up Your Store section above for a complete store setup example.
Simple Filtering
const query = new ChimeraQueryBuilder<Store, User, OperatorsMap>();
query
.where("email", "eq", "[email protected]")
.where("active", "eq", true)
.orderBy("createdAt", true);
const { filter, order } = query.build();Complex Logical Groups
const query = new ChimeraQueryBuilder<Store, Post, OperatorsMap>();
query
// Main AND condition
.where("published", "eq", true)
.where("createdAt", "gte", new Date("2024-01-01"))
// OR group: (authorId = '123' OR authorId = '456')
.group("or", (q) => {
q.where("authorId", "eq", "123").where("authorId", "eq", "456");
})
// NOT group: NOT (tags contains 'draft')
.group("not", (q) => {
q.where("tags", "contains", "draft");
})
.orderBy("createdAt", true)
.orderBy("title"); // Secondary sort
const { filter, order } = query.build();Multiple Order Rules
const query = new ChimeraQueryBuilder<Store, Product, OperatorsMap>();
query
.where("inStock", "eq", true)
.orderBy("category") // Primary sort: category ascending
.orderBy("price", false) // Secondary sort: price ascending
.orderBy("rating", true) // Tertiary sort: rating descending
.orderBy("name"); // Final sort: name ascending
const { filter, order } = query.build();Combining whereNot and Groups
const query = new ChimeraQueryBuilder<Store, Article, OperatorsMap>();
query
.where("published", "eq", true)
.whereNot("archived", "eq", true)
// Either in featured category OR has high views
.group("or", (q) => {
q.where("category", "eq", "featured").where("views", "gte", 1000);
})
// But NOT in any of these tags
.group("not", (q) => {
q.where("tags", "in", ["spam", "deprecated"]);
})
.orderBy("publishedAt", true);
const { filter, order } = query.build();Empty Query
const query = new ChimeraQueryBuilder<Store, Entity, OperatorsMap>();
// Build without any filters or ordering
const { filter, order } = query.build();
// filter: null
// order: nullChaining Pattern
All methods return this, allowing you to chain operations:
const query = new ChimeraQueryBuilder<Store, User, OperatorsMap>()
.where("active", "eq", true)
.where("role", "in", ["admin", "moderator"])
.group("or", (q) => {
q.where("age", "gte", 18).where("verified", "eq", true);
})
.orderBy("lastActive", true)
.orderBy("name");
const descriptor = query.build();Type Safety
The ChimeraQueryBuilder is fully type-safe. It ensures:
- Property names must exist on the entity type
- Operator names must exist in the
OperatorsMap - Test values match the expected types for each operator
- Property getters are correctly typed
type User = {
id: string;
name: string;
age: number;
email: string;
};
type OperatorsMap = {
eq: (value: any) => boolean;
gte: (value: number) => boolean;
in: (value: any[]) => boolean;
};
const query = new ChimeraQueryBuilder<Store, User, OperatorsMap>();
// ✅ Valid
query.where("name", "eq", "John");
query.where("age", "gte", 18);
query.where("id", "in", ["1", "2", "3"]);
// ❌ TypeScript errors
query.where("invalidProperty", "eq", "value"); // Property doesn't exist
query.where("age", "invalidOp", 18); // Operator doesn't exist
query.where("age", "eq", "not a number"); // Wrong type for operatorIntegration
With Repository
const query = new ChimeraQueryBuilder<typeof store, User, OperatorsMap>()
.where("active", "eq", true)
.orderBy("createdAt", true);
// Use with entity store
const users = userStore.getCollection(query.build());Notes
- Multiple
where()calls are combined with AND logic by default - Use
group('or', ...)to create OR conditions - Use
group('not', ...)orwhereNot()for negations - Order rules are applied in the sequence they're added (first = primary sort)
- An empty query builder will return
{ filter: null, order: null }
