blade.im
283

Query Instructions

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.

Asserting Fields (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.

Filtering Linked Records

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".

Advanced Assertions

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' },
});

Ordering (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'],
});

Applying Presets (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 Fields (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.

Filtering Fields

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'])

Mounting Fields (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' }

Joining Records

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.

Limited Amount (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.

Next Page (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.

Previous Page (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.