A JavaScript helper to find objects in a JavaScript array fast and with readable code. Can be used anywhere JavaScript runs. TypeScript support included.
Though you can do everything query provides you with the Array filter
method, query makes it more readable and
concise.
npm install array-query
Import or require it, or use it in the browser.
import { query } from "array-query";
const firstJacob = query("firstName").is("Jacob").first(users);
const { query } = require("array-query");
const allJacobs = query("name").startsWith("Jacob").on(users);
const { query } = ArrayQuery;
const noJacobs = query("name").not().startsWith("Jacob").on(users);
query
supports chaining with and
and or
and subqueries. Call the on
method with your array when done.
The following will return all users with the first name Bob whose last name is not Smith.
const result = query("firstName")
.is("Bob")
.and("lastName")
.not()
.is("Smith")
.on(users);
When you call query()
it creates a new query object which returns a reference to itself from each method. When the
query is executed by calling on(array)
it compiles a function with the logic of the query making it very fast. You
can re-use the same query. Because query chaining returns itself, the following three queries are all the same.
const q1 = query("author");
q1.is("Terence Hanbury White");
q1.and("title");
q1.is("The Once and Future King");
const q2 = query("author").is("Terence Hanbury White");
q2.and("title").is("The Once and Future King");
const q3 = query("author")
.is("Terence Hanbury White")
.and("title")
.is("The Once and Future King");
if ((q1.toString() == q2.toString()) == q3.toString())
alert("They're all the same!");
More examples:
const whiteBooks = query("author").is("Terence Hanbury White").on(books);
const theBooks = query("title").startsWith("The").on(books);
const bigBooks = query("pages").gt(500).on(books);
const topTenBiggestBooks = query()
.sort("pages")
.numeric()
.desc()
.limit(10)
.on(books);
If you'd rather start with the Array you may use the slightly different select(array)
API.
var aBooks = select(books).where("title").startsWith("A").end();
The two differences between query
and select
is that:
query
ends with the array (e.g..on(books)
) andselect
starts with it (e.g.select(books)
)select
needs to know when to be done chaining and to return the results, so it ends withend()
query
is the start of our query and may optionally take the first field we want to filter by. The query
method does
not need to take a field if you only want to sort or limit the objects.
query().sort("lastName").limit(20).on(users);
query("age").gt(20).on(users);
const scores = [5, 140, 23, 10, 829, 13, 4];
const highscores = query().sort().limit(3).on(scores);
The first query listed here shows using query()
without a parameter. It sorts by lastName and limits the results to 20
objects. The second query gets all the objects where age is greater than 20. The third sorts by the value itself.
query provides the ability to use and()
and or()
in putting together your query. These usually take a parameter,
which can either be a field name or another query object. The field name is only the beginning of an expression and when
used should be followed up with another method call such as equals()
, gt()
, etc.
query("username").equals("test").or("password").equals("test").on(users);
This looks up all objects whose username or password is "test".
Query objects may be used inside the methods and()
and or()
to provide subqueries. This is like putting parenthesis
around the expression.
const notMiddleAged = query("firstName").equals("John").and(query("age").lt(20).or("age").gt(60)));
This query allows us to find all objects where the firstName
is John and the age is either less than 20 or more than 60. We are unable to do this kind of sub-querying with the object-based API.
The not()
method can be used in an expression to negate the results. It can take a value which may make using is
more readable. The 2 following queries are the same.
query("age").not().gt(20).and("eyeColor").not().equals("blue").on(users);
query("age").not().gt(20).and("eyeColor").is().not("blue").on(users);
This query will get every object where age is not more than 20 and eye color is not blue.
equals
/is
are the most basic. The query should equal the value provided.
query("firstName").is("John").on(users);
query("lastName").equals("Smith").on(users);
This will match all objects where property firstName
equals "John". is
and equals
are synonymous.
within
tests whether the object's value is within a provided array of values.
query("firstName").within(["John", "Jacob", "Jingle", "Heimer"]).on(users);
This will match all objects whose firstName is "John", "Jacob", "Jingle", or "Heimer".
has
matches objects which have the provided value in an array.
users.push({ colors: ["red", "yellow", "blue"] });
const redOnes = query("colors").has("red").on(users);
This will match the previously added object since it's colors array has the value "red". Note that if on the stored
objects, colors is null or an empty array, it will not match since it doesn't have "red" in the colors array. It will
also not throw an error. query
's compiled function handles undefined values gracefully.
hasAll
matches objects which have all the provided values in an array.
query("colors").hasAll(["red", "blue"]).on(users);
This will match all objects which have both "red" and "blue" in their colors array.
startsWith
matches the beginning of a value.
query("firstName").startsWith("J").on(users);
This will match all objects whose firstName
begins with "J".
endsWith
matches the end of a value.
query("lastName").endsWith("son").on(users);
This will match all objects whose lastName
ends with "son".
gt
matches objects whose value is greater than what's provided. Dates are supported.
query("age").gt(20).on(users);
This will match objects with age
greater than 20;
gte
matches objects whose value is greater than or equal to what's provided. Dates are supported.
query("age").gte(20).on(users);
This will match objects with age
greater than or equal to 20;
lt
matches objects whose value is less than what's provided. Dates are supported.
query("age").lt(20).on(users);
This will match objects with age
less than 20;
lte
matches objects whose value is less than or equal to what's provided. Dates are supported.
query("age").lte(20).on(users);
This will match objects with age
less than or equal to 20;
test
/regex
matches objects whose values match the provided regular expression. They are aliases of each other.
query("name")
.test(/[^\w\s]/)
.on(users);
query("name")
.regex(/[^\w\s]/)
.on(users);
This will match objects that have a non word-or-space character in the name
property.
same
matches objects where the value is the same using a deep equal comparison. This allows arrays or objects to be
matched without a reference to the original. It can match partially as well.
users.push({ name: { first: "John", last: "Smith" }, age: 30 });
query("name").same({ first: "John", last: "Smith" }).on(users);
query().same({ name: { first: "John", last: "Smith" } }, { partial: true }).on(users);
This will match the added object since the name
value is the same even if it isn't the exact instance in memory. The
second query matches anyone with the same name object because partial
is set to true, even though age
is left out of
the matching object.
type
matches objects where the object or property is of a given type. Valid types are a string of: object, array, number, boolean, null, undefined. Or an instance of a class (e.g. Date). If no property name is passed into the query()
, and()
, or or()
methods then the type will match against the object itself rather than a property.
query("age").type("number").on(users);
query("published").type(Date).on(users);
query("pet").type(Dog).on(users);
query().type(User).or().type(Person).on(users); // matches if the object is and instance of User or Person (or a subclass thereof)
The first call will match all objects with a number for the age. The second call will match all objects where published
is an instance of Date. The third call will match all User
and Person
objects in the database.
filter
allows a custom filter function to be run against the value of a property or the object as a whole. If the
function returns true, the object is added to the query results.
query("firstName")
.filter(function (name) {
return name.toLowerCase().charAt(0) === "a";
})
.on(users);
query()
.("firstName").is("Jacob").and()
.filter(function (obj) {
if (obj instanceof User) {
return obj.active;
} else if (obj instanceof Person) {
return obj.trustLevel === "trusted";
} else {
return false;
}
})
.on(users);
The first query here uses a custom function to match against the value of the firstName
property of every object. The
second query uses a custom function to use custom logic to match against every object because no property name was
passed into the and()
function.
search
matches all objects with a pseudo full-text search on the given field. It will ensure that each word exists in
the value.
query("bio").search("looking for all of these words").on(users);
This will match any objects which have the provided words in their bio
field.
Sorts the returned results by property. asc()
and desc()
defined the order, and custom()
allows a custom sort
function. The default sort uses asc()
so these it don't need to be used explicitly. Custom allows sorting on a
property or on the object as a whole.
The default sort handles NaN
, null
, and undefined
values which are all sorted to the end (when using
ascending order) in this order. When using a custom sort, you must handle this yourself if it is a possibility.
Note: the sort methods to define the type of sort have been removed because they all did the same thing.
query("active").is(true).sort("lastName").on(users);
query().sort("lastName").desc().on(users);
query().sort("publishedDate").date().desc().sort("title").on(users);
query()
.sort("someNumericStringValue")
.custom((a, b) => {
if (a.length !== b.length) return a.length - b.length;
return a > b ? 1 : a < b ? -1 : 0
}).desc()
.on(users);
query()
.sort()
.custom((a, b) => a.age - b.age)
.on(users);
The first query sorts by lastName
after selecting only active objects. The second query sorts all objects by
lastName
in descending or reverse order. The third query sorts by publishDate
with most recent first, then by title
for dates that are the same. The fourth query uses a custom sort on a numeric string property (e.g. correctly sorts
"1"
, "100"
, and "2"
). The last query uses a custom sort on the object as a whole.
Limit the results returned.
query().limit(10).on(users);
This returns 10 objects from the top of the array.
query().sort("noisy").limit(10).on(users);
This returns 10 noisiest users (whatever that might mean).
Works with limit to select an offset which to start your limit at. Use for pagination.
query().limit(10).offset(100).on(users);
This returns 10 objects starting at the 100th object.
Query fields can be dot-delimited to match sub-properties. They may even use methods. Note that while query object does gracefully handle undefineds, you may still want to check for that first.
users.push({ name: "Bob", colors: ["red", "yellow", "blue"] });
query("colors.length").is(3).on(users);
query("colors.length").gt(2).on(users);
query("colors.length").lte(3).on(users);
These will all match the added object because the length is equal to three, greater than two, and less than or equal to three.
users.push({ colors: ["red", "yellow", "blue"] });
users.push({ });
query("colors.length").is(0).on(users); // will not match the newly added object because it is null
query("colors").is(undefined).or("colors.length").is(0).on(users); // this is how you should check
To check for a name without respect to case you might do the following.
query("firstName.toLowerCase()").is("bob");
query().sort("lastName.toLowerCase()");
The second query above will sort by last name irrespective of casing.
query was originally built with Backbone.js in mind. Though you may use
query("get("firstName")").is("John")
to effectively work with Backbone models, query allows you to shorten that to
just use firstName
as in query("firstName").is("John")
. You can even add methods like the previous section indicates
like: query("firstName.toLowerCase()").is("john")
. This works with any objects that have a get('property')
interface
such as EmberObject.
You may find it useful to add query to the Collection interface so that it is avaiable with every collection.
Note: The set
method was removed from query. To replicate this, you may either extend query or simply run your own
logic with the results like this:
query("selected").is(true).on(users).forEach(user => user.set("selected", false));