The third part of every Blade query is its instructions (“query instructions”).
Blade queries generally do not require instructions to be provided, as the purpose of instructions to clarify which records should be affected by given query, and to format how the records should be returned from Blade, meaning in which format.
A query without instructions may look like this, for example:
use.accounts();
Whereas a query with instructions would look like this:
use.accounts.with.email.containing('site.co');
Just like with the other levels of a Blade query, you may choose to nest query instructions in any way you like, meaning that you may use object-notation versus dot-notation on any level of your choice, in order to ensure the simplest query possible.
Below, you will find a list of all the different query instructions that are available, of which some are limited to specific query types.
with
)If you only need to apply simple assertions when querying records (meaning ensuring a field
matches a particular value directly, using “equals”), you can may choose to use the with
instruction:
use.account.with.id('acc_fa0k5kkw35fik9pu'); use.account.with({ lastName: 'Elaine', email: 'elaine@kojima-productions.com', });
As you can see above, the with
instruction accepts either a single field (in which case the
entire query will only span a single line) or multiple fields (in which case an object with multiple
properties can be passed instead).
For more complex assertions that will require a longer syntax, please refer to Advanced Assertions below.
When filtering linked records (fields of type "Link"), you can query their fields directly.
In the example below, space
is defined as a field of type “Record” that links to the "Space"
model. You can therefore directly apply instructions for the “Space” record as well:
use.team.with({ space: { handle: 'my-space', }, handle: 'my-team', });
In this example, we are asserting the handle
field of both the “Team” record and the nested
"Space" record. The query retrieves the first “Team” record whose handle
field
matches "my-team"
and whose space
field points to a record of the “Space” model that has
its handle
field set to "my-space"
.
If you would like to retrieve records where certain fields match certain values, the syntax shown above would be the simplest solution.
However, if you would like to assert the value of those fields in a specific way, instead of
just via “equals”, you may instead choose to use the advanced sub properties of the with
query
instruction, which allow for asserting the value of fields with special matching operators:
use.accounts.with.email.startingWith('elaine');
In total, the with query instruction offers 7 different operators for asserting fields:
use.accounts.with.email.being('elaine@twitter.com'); use.accounts.with.email.notBeing('elaine@twitter.com'); use.accounts.with.email.startingWith('elaine'); use.accounts.with.email.notStartingWith('elaine'); use.accounts.with.email.endingWith('@twitter.com'); use.accounts.with.email.notEndingWith('@twitter.com'); use.accounts.with.email.containing('twitter'); use.accounts.with.email.notContaining('twitter'); use.accounts.with.upgradedAt.greaterThan(new Date()); use.accounts.with.upgradedAt.greaterOrEqual(new Date()); use.accounts.with.upgradedAt.lessThan(new Date()); use.accounts.with.upgradedAt.lessOrEqual(new Date());
As with all other query instructions, you may also choose to nest the operators in different ways, which allows for asserting multiple fields at once, if necessary:
use.accounts.with({ lastName: { being: 'Marksman' }, email: { endingWith: '@twitter.com' }, });
orderedBy
)Regardless of whether a singular or multiple records are being retrieved, the returned records
will always be ordered by their creation date (the ronin.createdAt
field) by default, meaning
that the most recently created records will be returned first.
// The most recently created record of the model will be returned. use.customer(); // The 20 most recently created records of the model will be returned. use.customers(); // The most recently created record of the model and matching the provided // conditions will be returned. use.customer.with.country('germany'); // The 20 most recently created records of the model and matching the provided // conditions will be returned. use.customers.with.country('germany');
In order to define a custom order for the returned records, you may optionally provide
the orderedBy
instruction for any query of type get
.
Blade offers the default fields ronin.createdAt
and ronin.updatedAt
for easily ordering records
using their creation and update date:
// The 20 most recently updated records of the model will be returned. use.blogPosts.orderedBy.ascending(['ronin.updatedAt']); // The 20 least recently updated records of the model will be returned. use.blogPosts.orderedBy.descending(['ronin.updatedAt']);
If needed, you may also order by multiple different fields in different ways:
use.blogPosts.orderedBy({ ascending: ['title', 'ronin.updatedAt'], descending: ['slug'], });
using
)To quickly resolve the target record of link fields, you can apply a default preset to your query, which joins the target record onto the record that you are retrieving.
If the slug of the link field is account
, for example, you could resolve it like this:
use.member.using(['account']);
Based on which fields you've defined for your model, several different presets are available to you by default, and it is recommended to make as much use of them as possible, in order to keep your queries lean and concise.
Furthermore, you can customize the list of presets on a per-model basis, and add additional ones in case you would like to reuse the same query in multiple places across your application.
selecting
)By default, every record you query contains all fields that are stored for it in the database.
This behavior makes it easy to pass records around in your application, since every record always contains all available information. Furthermore, Blade is designed in such a way that the performance of your application does not change, regardless of whether you obtain all fields of a record, or only some of them.
Nevertheless, you might want to restrict which fields are provided to your application, which can
be done using the selecting
instruction:
use.account({ with: { handle: 'elaine' }, selecting: ['name'], });
The query shown above will still return an object for your record, but that object will now only
contain a single field, which is name
.
Selecting specific fields can be beneficial if the fields you exclude hold extremely large values, since those values then no longer need to be transferred into the rendering pipeline of your application every time your page renders or gets updated.
Similarily, if the fields you exclude contain sensitive data (such as a hashed password), the security of your application would be strengthened as well, since the sensitive field would then never leave the database and never be exposed to your application.
Lastly, selecting individual fields can be beneficial when using sub queries, since those often require matching against a particular field of the sub query:
use.account.with({ email: use.invitation.selecting(['email']).orderedBy({ descending: ['joinedAt' ]}) });
For example, the query above would retrieve an account
record whose email
field matches the
email
field of the invitation
record with the most recent joinedAt
date field.
Every field slug you provide to the selecting
instruction is matched as-is, meaning only the
field whose slug matches the provided string 1:1 is returned in the final object.
If you would like to obtain multiple different fields that all match a particular pattern, you don't need to specify the field slug for each one. Instead, you can use a glob-inspired syntax for matching the fields:
// All fields at the root level of the record selecting(['*']) // All fields at the root level of the record that start with "na" selecting(['na*']) // All fields at the root level of the record that end with "ame" selecting(['*ame']) // All fields at the root level of the record, except for the "id" field selecting(['*', '!id']) // All fields at any level of the record selecting(['**']) // All fields at any level of the record that start with "ronin." selecting(['ronin.**']) // All fields at any level of the record that end with ".createdAt" selecting(['**.createdAt']) // All fields at any level of the record, except for the "id" field selecting(['**', '!id'])
Just like if you were to provide a list of bare field slugs (e.g. ['name', 'handle']
), a list
of glob filters is additive. Meaning that every subsequent array item refines the final object
further. As a result, you could even combine the filters with bare fields:
// All fields at any level of the record, except for the "id" field, // plus we want to include the `name` field specifically selecting(['**', '!id', 'name'])
including
)Every query returns all fields stored in the database for the record that is being
retrieved, unless you choose to limit which fields get returned using
the selecting
instruction. The values of those
fields match the exact values stored in the database.
If you would like to add additional properties to the record object you've
retrieved or replace the values of existing properties, you can use the
including
instruction, which lets you mount arbitrary fields with arbitrary values.
For example, let's consider the following model:
export const Team = model({ slug: 'team', fields: { name: string() } });
As you can see, we only store a field called name
in the database. The model does
not have any other fields. If we now retrieve a record for this model, we would
retrieve an object like the following one:
const team = use.team(); // { name: 'Engineering' }
Assuming that we want to see another field called handle
, but we don't want to
store that field in the database, we can simply attach it like this:
const team = use.team.including.handle('engineering'); // { name: 'Engineering', handle: 'engineering' }
The same would work with multiple fields, of course:
const team = use.team.including({ handle: 'engineering', company: 'acme' }); // { name: 'Engineering', handle: 'engineering', company: 'acme' }
Instead of mounting simple field values (described above), we can also mount entire records as fields on the parent record. The SQL equivalent of this behavior is called a "Join".
In the majority of cases, if you would like to resolve a record that is
related to the current record via a Link field,
there is no need for you to manually join any records. Instead, you only
need to provide a using: ['field']
instruction to your query, where
field
is the slug of the Link field for which you want to resolve the
related record.
If you need to join a record for whose model you did not establish a
relationship using a Link field, or you need to
customize the join in a special way, you can join the unrelated record
by placing a fully-fledged Sub Query in the
including
instruction:
const team = use.team.including(f => ({ handle: 'engineering', company: use.company.with.name(f.company) })); // { // name: 'Engineering', // handle: 'engineering', // company: { id: '1234', name: 'acme' } // }
Behind the curtains, Blade will automatically, based on the instructions provided to the parent query and the sub query, determine whether to generate an SQL Join or an SQL Sub Query (since the Blade query syntax compiles to SQL), in order to maximize performance.
limitedTo
)By default, 20 records are provided per page and more records can be obtained by
paginating them with the usePagination()
hook, meaning
by loading more pages.
However, in special cases in which you would like to retrieve more than 20 records from
Blade without having to load multiple pages, you may decide to use the limitedTo
instruction
to provide a custom page length:
use.accounts.limitedTo(50);
If you would like to display an infinite amount of records in your UI (for example displaying
the list of members of a team in your app, which might be allowed to be infinite), we strongly
recommend using pagination due to the reasons mentioned in
the usePagination()
section. In those cases, you should therefore
only resort to limitedTo
if you want to decrease or increase the page size slightly, not to
retrieve all records.
If, however, there is a guarantee within the conceptual model of your application that a certain
kind of record can only exist a finite amount of times (or to be specific, only a finite amount
or less than that is always displayed), you may decide to use limitedTo
in order to retrieve all
records at once.
The maximum value allowed by limitedTo
(the maximum page length) is 1000
. We strongly advice
against making use of such a high value unless truly necessary.
after
)Since the usePagination()
hook offers automatic
pagination, there is no need to use the after
instruction manually. In the rare case that the
hook is not flexible enough, however, you may use the instruction instead. Under the
hood, usePagination()
generates the instruction automatically.
When running a query such as use.accounts()
while more than 20 records are available for the
respective model, a moreAfter
property will be provided to you (the property only exists if
more than 20 records are available; otherwise it is not defined).
This property contains a so-called “cursor” pointing to the next page of records:
const accounts = use.accounts(); // Contains the cursor of the next page. accounts.moreAfter;
Whenever you would like to load more records, you can then pass the value of moreAfter
back
to Blade, and you will be provided with the next 20 records:
const moreAccounts = use.accounts.after(accounts.moreAfter); // Contains the cursor of the next page. moreAccounts.moreAfter;
You can repeat the above as often as you want to until there are no more records available (in
which case moreAfter
will not be defined anymore). Every time you run the query, a
new moreAfter
cursor will be provided to you.
before
)Since the usePagination()
hook offers automatic
pagination, there is no need to use the before
instruction manually. In the rare case that the
hook is not flexible enough, however, you may use the instruction instead. Under the
hood, usePagination()
generates the instruction automatically.
If you would like to implement bi-directional pagination, you may use the before
instruction
and its moreBefore
counterpart, which behave exactly the same as after
and moreAfter
, except
that they let you paginate “upwards” instead of “downwards”:
const moreAccounts = use.accounts.before(accounts.moreBefore); // Contains the cursor of the next page (upwards). moreAccounts.moreBefore;
For example, this would be useful if you've implemented a page that shows a specific range of
records that still has more records before and after it in Blade, which aren't displayed. You
could then use before
to reveal more of the “previous records” in the list.