querier-ts
A lightweight, type-safe in-memory query engine for JavaScript and TypeScript.
Query
Usage:
const usersQuery = Query.from(users);
TypeScript will automatically infer the query data type. You can also make it explicit:
const usersQuery = Query.from<User>(users);
Filtering data
where(condition)
This method accepts two types of parameters:
- An object, where each property represents a field to be validated.
- A callback function that returns a boolean.
When passing an object, each property can be either a literal value or a callback function used for validation.
const activeGmailUsers = Query.from(users)
.where({
isActive: true,
email: (email) => email.endsWith('@gmail.com'),
})
.where((user) => !user.isAdmin())
.all();
It also supports nested objects:
.where({
permissions: {
sendNotifications: true,
},
})
filterWhere(condition)
Unlike where(), filterWhere():
- Accepts only an object
- Ignores properties whose values are
null or undefined
let isActive: boolean;
const filteredUsers = Query.from(users)
.filterWhere({
id: 1,
isActive: isActive,
})
.all();
Note: If you need to explicitly check for null or undefined, use where() instead.
distinct(key | callback)
Removes duplicate rows based on a key or a callback function.
key: The key to be used for comparison.
callback: A function that maps each row to a value.
const uniqueCountries = Query.from(addresses)
.distinct('country')
.column('country');
const uniqueEmailProviders = Query.from(users)
.distinct((user) => user.email.split('@')[1])
.column('email');
Selecting data
select(columns)
Defines which columns should be selected, returning a new query with rows containing only the selected columns.
Accepts multiple column names.
.select('id', 'email')
map(callback)
Transforms each row into a new object using a callback function.
Like select(), this method is intended for data projection, but it is more flexible and better suited for complex transformations:
Callback arguments:
row: The current row.
index: The index of the current row.
const authorsWithFirstPostQuery = Query.from(authors)
.map(({ posts, ...author }) => ({
...author
firstPost: posts[0],
}));
When dealing with simple scenarios, prefer select():
const query = Query.from(users)
.map((user) => ({
id: user.id,
email: user.email,
}));
const query = Query.from()
.select('id', 'email');
Ordering results
orderBy(...columns | (callback, order?))
Sorts the results.
columns: the columns to order by.
callback: A function that maps each row to a value.
order: The order to sort in.
You can specify column names or a callback function to map each row to a value.
Examples:
.orderBy('name', '-createdAt')
.orderBy((user) => user.address.country)
.orderBy((user) => user.address.country, 'asc')
.orderBy((user) => user.address.country, 'desc')
Paginating results
skip(numberOfRows)
Skips the first results.
.skip(5)
Example:
const secondId = Query.from(users)
.select('id')
.skip(1)
.scalar();
Passing a non-integer or a negative number will throw an InvalidArgumentError.
limit(limit)
Limits the number of results returned.
.limit(100)
Passing a non-integer or a negative number will throw an InvalidArgumentError.
limitPerGroup(key | fn, limit)
Limits the number of rows per group.
key: The property name to group by.
fn: A function that maps each row to a grouping value.
limit: The maximum number of rows to keep per group.
⚠️ The rows kept depend on the current ordering of the query.
Use orderBy() beforehand to control which rows are selected.
Examples:
const countries = Query.from(addresses)
.orderBy('-createdAt')
.limitPerGroup('country', 2)
.column('country');
const countries = Query.from(addresses)
.orderBy('-createdAt')
.limitPerGroup((row) => row.country, 2)
.column('country');
Passing a non-integer or a negative number to limit will throw an InvalidArgumentError.
top(limit, options?)
Keeps the top N rows, optionally partitioned by a key or callback.
limit: The maximum number of rows to keep.
options.partitionBy: A property name or function to define groups.
options.orderBy: A column or list of columns to define ordering.
If partitionBy is provided, the limit is applied per group.
The rows kept depend on the ordering. Use orderBy (either here or before)
to control which rows are selected.
Examples:
const ids = Query.from(addresses)
.orderBy('-createdAt')
.top(3)
.column('id');
const countries = Query.from(addresses)
.top(2, {
partitionBy: 'country',
orderBy: '-createdAt',
})
.column('country');
const countries = Query.from(addresses)
.top(1, {
partitionBy: (row) => row.country,
orderBy: '-createdAt',
})
.column('country');
const countries = Query.from(addresses)
.top(1, { partitionBy: 'country' })
.column('country');
Passing a non-integer or a negative number to limit will throw an InvalidArgumentError.
Getting results
all()
Returns all results.
first()
Returns the first result.
last()
Returns the last result.
count()
Returns the number of results.
exists()
Returns a boolean indicating whether any results exist.
scalar()
Returns the value of the first property of the first result.
const firstId = Query.from(users).scalar();
You can combine it with select() to retrieve a specific property:
const firstEmail = Query.from(users)
.select('email')
.scalar();
Returns false if no value is found.
column()
Returns the values of the first property from all results by default.
const ids = Query.from(users).column();
You can specify a column:
const emails = Query.from(users).column('email');
Or combine it with select():
const emails = Query.from(users)
.select('email')
.column();
values()
Returns all results as arrays of values.
const data = Query.from(users)
.select(['id', 'email'])
.values();
Example output:
[
[1, 'john@icloud.com'],
[2, 'mary@gmail.com']
]
groupBy(key | callback, mapFn?)
Groups the items by a specified property or a custom callback and returns the result as a Map.
key: The property name to group by.
callback: A function that returns the value to group each item by.
mapFn (optional): A function to transform each item before adding it to the grouped result.
The returned Map uses the resolved grouping values as keys and arrays of matching (or mapped) items as values.
const usersByActiveStatus = Query.from(users).groupBy('isActive');
const idsByActiveStatus = Query.from(users).groupBy(
'isActive',
(user) => user.id
);
const usersByActiveStatus = Query.from(users).groupBy(
(user) => user.isActive
);
const idsByNotificationPreference = Query.from(users).groupBy(
(user) => user.permissions.sendNotifications,
(user) => user.id
);
Aggregating data
min(key | callback)
Returns the minimum numeric value from the results.
key: The numeric column to evaluate.
callback: A function that maps each row to a number.
Returns null if no rows exist.
const minAge = Query.from(users).min('age');
const minNameLength = Query.from(users).min(
(user) => user.name.length
);
max(key | callback)
Returns the maximum numeric value from the results.
key: The numeric column to evaluate.
callback: A function that maps each row to a number.
Returns null if no rows exist.
const maxAge = Query.from(users).max('age');
const maxNameLength = Query.from(users).max(
(user) => user.name.length
);
sum(key | callback)
Returns the sum of numeric values from the results.
key: The numeric column to evaluate.
callback: A function that maps each row to a number.
Returns 0 if no rows exist.
const totalAge = Query.from(users).sum('age');
const totalNameLength = Query.from(users).sum(
(user) => user.name.length
);
average(key | callback)
Returns the average of numeric values from the results.
key: The numeric column to evaluate.
callback: A function that maps each row to a number.
Returns null if no rows exist.
const averageAge = Query.from(users).average('age');
const averageNameLength = Query.from(users).average(
(user) => user.name.length
);