blade.im
283

Queries

Programmatically interacting with data in Blade is made possible through its unique query syntax, designed to allow for accessing data in the most “humane” way possible, as it closely mimics the English language.

Query Syntax Preview

To retrieve a list of records, for example, you could run the following:

export default function Page() {
  const accounts = use.accounts();

  return <div>I am a page</div>;
};

If you’d like to retrieve only a specific record, you could do so like this:

use.account.with.handle('elaine');

As you can probably already tell, Blade’s query syntax is quite straightforward to use while still providing enough flexibility to scale gradually as more advanced assertions are desired.

Components

Below, you can find a list of all the different parts of the Blade query syntax.

Query Type

Learn more about how to perform different actions with queries:

Query Target

The second part of every Blade query is its target (“query target”).

Every query is aimed at a particular model, which is addressed using either the Slug or the Plural Slug of the respective model. Both are located in your model definition. The slug attribute of models is always required, and the pluralSlug is auto-generated, but can be overwritten explicitly.

  • Using the Slug will result in a singular record getting addressed. Specifically, the most recently created record that matches the query.

  • Using the Plural Slug will result in multiple records getting addressed. Specifically, all existing records that match the provided query, ordered by their creation date.

Limitations

  • Queries of type add currently only support providing a Slug, not a Plural Slug, as only one record can be created at a time. You may create multiple records at once, by executing multiple queries. A query such as add.accounts() is therefore not supported, while add.account() is supported.

  • Queries of type count only support providing a Plural Slug, not a Slug, as the query type is used for counting records, and if there is a guarantee that there will be only a single record, counting them is not needed. A query such as count.blogPost() is therefore not supported, while count.blogPosts() is supported.

Query Instructions

Learn more about how to access specific records and adjust the behavior of the query:

  • Asserting Fields (with)

  • Paginating Records (before, after, limitedTo)

  • Ordering Records (orderedBy)

  • Resolving Related Records (using)

  • Selecting Fields (selecting)

Composability

As Blade's query syntax mimics a plain JavaScript or JSON object in its form, you may choose to expand or compress the individual levels of the object at any position of your choice.

For example, both of these queries perform exactly the same action:

use.blogPosts.with.slug('new-pricing');

use.blogPosts({
  with: { slug: 'new-pricing' },
});

Any level that contains a period (.) can instead be a nested object if you decide so. This allows you to structure the query in any way you like, to keep it as simple as possible and as human-readable as possible, even as complexity increases.

Function Chaining

To keep your query as short as possible, you can also chain its function calls.

While doing so, you only need to keep one rule in mind: All properties for which you perform function calls must be on the same level in the "object" of the query.

For example, you could run the following query:

use.members.with({ org: 'acme' }).orderedBy({ descending: ['invitedAt' ]});

The query above would be the same as:

use.members({
  with: { org: 'acme' },
  orderedBy: { descending: ['invitedAt'] }
});

As long as you make sure that the function calls all happen at the same level, it doesn't matter at which level of the "query object" you perform them. For example, this would work too:

use.members.with.org('acme').team('engineering');

And it would be the same as writing:

use.members.with({
  org: 'acme',
  team: 'engineering'
});

Dot Notation

Additional flexibility is provided as every key inside the object can contain dot notation as well.

This is especially useful when addressing nested fields (either of a relational record or the Blade metadata) or when writing extremely sophisticated queries, as the syntax continues to remain readable and even easy to augment with comments.

For example, the query below retrieves all records of the model “Blog Post” where the author is matched using a given username/handle, and the record is older than a given date:

use.blogPosts({
  with: {
    author: { handle: { being: 'elaine' } },
    ronin: { createdAt: { lessThan: new Date() } },
  },
  using: ['author'],
});

In order to simplify addressing nested fields, Blade supports “dot notation”, which may be used on any level of the query:

use.blogPosts({
  'with.author.handle.being': 'elaine',
  'with.ronin.createdAt.lessThan': new Date(),
  using: ['author'],
});

Another example of placing the “dot notation” on a different level could be:

use.blogPosts.with({
  'author.handle': 'elaine',
  'author.email': 'elaine@kojima-productions.com',
});

Similarily, if you’d like to provide comment augmentation for extremely sophisticated queries, you can easily do so like this:

use.teams.with({
  // Only retrieve teams of the current Space.
  'space.handle.being': spaceHandle,

  // Exclude the current team.
  'handle.notBeing': teamHandle,

  // Exclude children of the current team.
  team: [{ 'handle.notBeing': teamHandle }, { being: null }],
});

OR Conditions

In certain cases, you might want to express an “OR” condition in Blade’s query syntax, by requiring one of two (or more) possible sub-conditions to match.

Achieving this is only a matter of using Arrays, rather than Objects, for your queries.

You can think about Blade’s query syntax in the following way:

  • Objects require every nested property within them to match.

  • Arrays require at least one nested item within them to match.

In the following example, we want to retrieve a record of the “Blog Post” model for which the “author” field matches at least one of two possible values:

use.blogPost.with.author(['acc_vais0g9rrk995tz0', 'acc_fa0k5kkw35fik9pu']);

The array syntax can currently be applied to any level inside the with query instruction:

use.accounts.with.handle(['leo', 'juri']);

use.accounts.with.handle([{ endingWith: 'test' }, { startingWith: '1234' }]);

use.accounts.with([{ handle: { being: 'today' } }, { name: { endingWith: 'lamprecht' } }]);

You can even use it on multiple different nesting levels at once:

use.accounts.with([{ handle: { being: ['juri', 'leo'] } }, { name: { endingWith: 'lamprecht' } }]);

Sub Queries

If models in your database schema are related to each other, it is recommended to define those relationships using Link fields instead of using primitive field types such as String.

Doing so lets the query syntax behave more intelligently when you interact with the records of those models, and thereby enables you to write shorter queries.

For example, let's assume your database schema contains the following models:

export const Account = model({
  slug: 'account',

  fields: {
    handle: string()
  }
});

export const Member = model({
  slug: 'member',

  fields:
    account: link({ target: 'account' })
  }
});

Because you've established a relationship between the two models using a Link field, you could now run queries like the following:

// Add a new account record
await add.account.with.handle('elaine');

// Add a new member record
await add.member.with.account({ handle: 'elaine' });

As you can see above, the first query is quite "boring" in the sense that it simply adds a new account record whose handle field has a value of "elaine". That's it.

The second query, however, is the interesting one. It creates a new member record whose account field targets the record we previously created. But instead of passing the ID of that record, it provides an object of fields, and that object is used to resolve the target record. In other words, the target record is first resolved using the provided object, and then the ID of the record that was found is stored as the value of the account field.

The provided object follows the syntax of the with instruction.

This makes your code more readable, because you don't need to pass the result of the first query to the second query, but it also ensures the best possible performance, since it avoids a request waterfall to your database. Meaning instead of sending two serial requests, only one with two queries inside will be sent, which is faster.

You could therefore even bundle them up into a single transaction, which is necessary if you want to execute two queries at once:

await batch(() => [
  add.account.with.handle('elaine'),
  add.member.with.account({ handle: 'elaine' })
]);

Thanks to Dot Notation, you could even make the second query look more similar to the first one, if you are into especially clean code:

await batch(() => [
  add.account.with.handle('elaine'),
  add.member.with.account.handle('elaine')
]);

The same syntax also works with any other Query Type, meaning you can use it when adding records, retrying, modifying, removing, or even counting them:

await set.member({
  with: { id: '1234' },
  to: { account: { handle: 'elaine' } }
});

If you are writing records and the related record you are trying to resolve does not exist, the value of the Link field will be null. There currently isn't a way to make the query fail if the related record doesn't exist, but such a feature is planned.

Unrelated Records

If the built-in syntax for automatically resolving records via Link fields (described above) is not sufficient for you, you can provide fullly-fledged sub queries instead of only providing the object for asserting fields of the target record.

For the other examples above, we had been applying mutations. For the next example, we will read records instead, and use a sub query to filter the records. The same syntax can be applied to any kind of query, however.

use.messages(() => ({
  with: {
    pending: false,
    author: use.author({
      with: { email: { containing: 'site.co' }},
      selecting: ['id']
    })
  },
  orderedBy: {
    ascending: ['readAt']
  }
}));

The query above retrieves all records of the "message" model whose pending field is false and whose author field matches a record of the "author" model that has an email field containing the string "@site.co".

The key requirement in order for a sub query to work is () => ({}), meaning the arrow function you can see in the example above. It tells all nested queries to inline themselves into the parent query instead of getting executed. Since executing the nested queries would cause a data waterfall (multiple requests to the database), the query is instead merged/inlined into the representation of the parent query, which means that only single large query is produced and executed for the entire code shown above.

Field References

To access other fields from within a query, you can make use of a function argument that is provided to every function call of that query.

Specifically, only a single argument is provided to those function calls, and that argument holds an object whose properties are the references to all fields available within the current model.

use.accounts(f => ({
  with: {
    fullName: concat(f.firstName, ' ', f.lastName)
  }
}));

As an example, the query above retrieves all account records whose fullName field contains a string that matches a combination of the firstName and lastName fields being concatenated, which is done using a query function.

Referencing fields of the current query can be especially useful with sub queries, because you can use them to reference fields of the parent query inside the sub query:

use.account(f => ({
  email: use.invitation.with({ name: f.name }).selecting(['email'])
}));

The query above retrieves an account record for which an invitation record exists with the same name field, without performing any data waterfalls.