blade.im
283

Models

The schema of your database consistents of multiple models. Each model uses fields to define the shape of a particular kind of record in your database.

For example, your database schema might contain a model called "Account" representing a user, and a model called "Team", representing a group of users.

When using Blade, your database schema is defined in code, which provides maximum flexibility and benefits such as revisions, reviews and reverts via Git. This page provides an overview of how to define your database schema in code.

Defining Models

At the root of your project, start by creating the file that will contain your model definitions:

mkdir schema
touch schema/index.ts

A model is defined using the model() function provided under the blade/schema path. The path also provides all field type primitives, such as string(), number(), or blob().

Here is an example of a basic schema definition:

import { model, string, date } from 'blade/schema';

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

  fields: {
    name: string(),
    handle: string(),
    verifiedAt: date()
  }
});

In the example above, we defined a model named Account with three fields: name, handle, and verifiedAt. Each field is defined using a key-value pair where the key is the slug of the field and the value is the type of the field. See all available field types in the Field Types section.

Once you've defined your models, you can compare them to the current state of your database:

blade diff

Running the command above will produce a so-called "migration protocol" in the .blade directory. This protocol contains all the steps required to update your database to match your local schema definition.

To apply the changes to your database, run the following command:

blade apply

Afterward, you can start sending queries to your database that make use of the newly added (or updated) models in your database schema.

In the future, if you'd like to immediately apply your local changes to your database without first reviewing them, run this command:

blade diff --apply

Like this, the generated migration protocol will be applied immediately, without you being able to review it first. This can be especially useful during development as it maximizes efficiency, but we recommend against applying migrations without reviewing them in production.

In general, when working in a team, you most likely want to run blade diff in order to generate a migration protocol for your change, commit it to a pull request together with your schema change, have it be reviewed by your team, and then run blade apply after the pull request was approved.

An update will also be provided in the future to automatically apply migrations that were merged onto the main branch of your repository, so that you don't need to run blade apply yourself.

Fields

The following types can be used to define fields in a model:

FunctionType
string()String field
number()Number field
boolean()Boolean field
date()Timestamp field
blob()Binary object field
json()JSON field

Learn more about how to apply the individual field types.

Presets

As your application grows, you might find yourself repeating similar queries in multiple different places across the application. Such duplicated code can be avoided by creating utility files that can be imported in all the places where the reusable query instructions are needed.

Such utility files are great for reusing entire queries and attaching additional React hooks to them, to make them rely on other input, such as cookies or the URL of the current page. If you only want to reuse specific instructions of a query and don't need to rely on other input, however, query presets might be more suitable for you.

Since query presets are defined as part of your database schema, they can also be reused across multiple different Blade applications that are accessing the same database, without having to share code for potential utility files between them.

You could therefore consider views to be the SQL equivalent to presets in Blade. The key difference is that views in SQL represent entire queries, whereas in Blade, presets represent only a part of the query (the query instructions), which allows multiple presets to co-exist on the same level.

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

  fields: {
    pending: boolean(),
    team: string()
  },

  presets: {
    active: {
      instructions: { with: { pending: false, team: null } }
    }
  }
});

The model above contains a preset with the slug active that uses the with instruction to apply filters for two different fields, which allows us to query exactly the kinds of records we need, without having to re-define those filters every time.

use.member.using(['active']);

In order to generate custom query instructions in the shortest way possible, we can also add a dynamic value to the preset, which can be provided with every query:

activeAccount: {
  instructions: {
    with: { pending: false, team: null, account: QUERY_SYMBOLS.VALUE }
  }
}

In the query, we could then provide the value like this:

use.member.using({ activeAccount: '1234' });

Default Presets

Just like every model contains a list of default fields, it also contains a list of default presets that are automatically generated for those fields.

With those default presets, you can quickly write extremely short queries that effectively produce the desired result, without needing to write out detailed query instructions.

Specifically, for every link field defined in your model, a preset with the same slug is generated that joins the target record if you invoke it. Let's assume, for example, that our model looks like this:

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

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

Behind the curtains, Blade would then generate the following preset for you:

account: {
  instructions: {
    including: {
      account: use.account.with.id(f.account)
    },
  },
  name: 'Account'
}

Thanks to this auto-generated preset, you can write an extremely short query for joining the target record of your link field:

use.member.using(['account']);

// { id: '6532', account: { id: '2439', name: 'Elaine' } }

In fact, Blade even auto-generates a second preset for your link fields, however that second preset is located on the target model, which lets you easily query the "child records", meaning the records that are pointing to the records on the target model:

use.account.using(['members']);

// {
//  id: '2439',
//  members: [
//    { id: '6532', account: '2439' },
//    { id: '53i9', account: '2439' }
//  ]
// }

The behavior mentioned above also applies if you've set the cardinality of your link field to many to make it target multiple records instead of a single one. The only difference is that, for those fields, only a single default preset will be generated, which is the first default preset mentioned above. The second default preset (generated on the target model) will not exist.

TypeScript Types

The queries defined across your application automatically receive TypeScript types that are based on your model definitions. Those types are stored in a file named .blade/types.d.ts and are updated every time your run blade apply.

To prevent the types from getting updated automatically, you can pass a --skip-types flag to the blade apply command. You could then use the separate blade types command to generate fresh TypeScript types from your model definitions whenever you need them.

Assuming that your application is located in a Git repository for which an automatic CI process with type-checking runs on a Git provider, we advice committing the .blade directory to your repository, such that your CI can validate the TypeScript types.

In general, however, the .blade directory is ephemeral, meaning it does not need to be committed to your Git repository, since it is automatically re-generated every time you run a migration command (blade diff or blade apply) in your project.

Formatting

Slugs of models and fields must be provided in camel case, since Blade queries are written in TypeScript and camel case is the common formatting choice for property names in the TypeScript ecosystem.

For example, the following slug would be allowed for models:

productCategory

While the following are not allowed:

product_category
product category
ProductCategory

Multiple Apps

If you are maintaining multiple Blade applications that are accessing the same database, make sure to designate only a single application as the "source of truth" in your team. In other words, the commands blade diff and blade apply should only be run in a single application per database.

After every update to the database schema, you could then run blade pull in all other applications where that same database schema is used, which will re-generate the local model definitions from the current state of the remote database.

A future update to Blade will make it possible to run queries from multiple apps without repeating the database schema source code in each one of those applications.