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.
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.
The following types can be used to define fields in a model:
Function | Type |
---|---|
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.
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' });
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.
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.
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
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.