Schemas
Schemaful vs Schemaless
Providing a schema to Triplit is optional, but it is recommended in order to take advantage of all the features provided by Triplit.
Limitations of schemaless mode include:
- You are limited to exclusively using storing value types that are supported by JSON: string, number, boolean, objects, null.
- If you use Typescript, you will not get type checking for your queries and results.
- Access control rules are defined in schemas, and thus are not supported in schemaless mode.
Defining your schema
A schema object defines your collections and the attributes and relationships on those collections. Schemas are defined in Javascript like so:
import { Schema as S } from '@triplit/db';
import { TriplitClient, ClientSchema } from '@triplit/client';
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
complete: S.Boolean(),
created_at: S.Date(),
tags: S.Set(S.String()),
}),
},
users: {
schema: S.Schema({
id: S.Id(),
name: S.String(),
address: S.Record({
street: S.String(),
city: S.String(),
state: S.String(),
zip: S.String(),
}),
}),
},
} satisfies ClientSchema;
const client = new TriplitClient({
schema,
});
Passing a schema to the client constructor will override any schema currently stored in your cache. This can cause data corruption, if the new schema is not compatible with existing data in the shape of the old schema. Refer to the schema management guide for more information.
id
Every collection in Triplit must define an id
field in its schema. The S.Id()
data type will generate a random id
by default upon insertion. If you want to specify the id
for each entity, you may pass it as a string in to the insert
method as shown below.
// assigning the id automatically
await client.insert('todos', {
text: 'get tortillas',
complete: false,
created_at: new Date(),
tags: new Set([groceries]),
})
// assigning the id manually
await client.insert('todos', {
id: 'tortillas'
text: 'get tortillas',
complete: false,
created_at: new Date(),
tags: new Set([groceries]),
})
Getting types from your schema
While the schema
passed to the client constructor will be used to validate your queries and give you type hinting in any of the client's methods, you may want to extract the types from your schema to use in other parts of your application. You can do this with the Entity
type.
import { Entity, ClientSchema } from '@triplit/client';
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
complete: S.Boolean(),
created_at: S.Date(),
tags: S.Set(S.String()),
}),
},
} satisfies ClientSchema;
type Todo = Entity<typeof schema, 'todos'>;
/*
Todo will be a simple type:
{
id: string,
text: string,
complete: boolean,
created_at: Date,
tags: Set<string>
}
*/
Data types
When using a schema you have a few datatypes at your disposal:
Schema type
The schema type is used to define a collection and its attributes. It should always be the outermost type in your schema definition. It is a special type of Record type so may use the same modifiers as Record.
import { Schema as S } from '@triplit/db';
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
// Additional attributes here...
}),
},
};
Value types
Value types are basic primitive types for the database.
String
The string data type is used to store text.
import { Schema as S } from '@triplit/db';
const stringType = S.String();
Strings support =
, !=
, like
and nlike
operators in where
statements.
You can use the like
operator in a where clause to do simple filtering with string similarity. A like
expression is true if the supplied attribute matches the supplied filter pattern.
An underscore (_
) in a pattern stands for (matches) any single character; a percent sign (%
) matches any sequence of zero or more characters.
For example:
['triplit', 'like', 'triplit'] true
['triplit', 'like', 'tri%'] true
['triplit', 'like', 'tr_pl_t'] true
['triplit', 'like', 'trip'] false
Number
The number data type is used to store integer or float numbers.
import { Schema as S } from '@triplit/db';
const numberType = S.Number();
Numbers support =
, !=
, >
, >=
, <
, <=
operators in where
statements.
Boolean
The boolean data type is used to store true or false values.
import { Schema as S } from '@triplit/db';
const booleanType = S.Boolean();
Booleans support =
, !=
operators in where
statements.
Date
The date data type is used to store date and time values.
import { Schema as S } from '@triplit/db';
const dateType = S.Date();
Dates support =
, !=
, >
, >=
, <
, <=
operators in where
statements.
Options
Value types have a few options that can be passed to their constructor.
nullable
You can indicate an attribute is nullable by passing the { nullable: true }
option to its constructor.
import { Schema as S } from '@triplit/db';
import { TriplitClient } from '@triplit/client';
const schema = {
test: {
schema: S.Schema({
id: S.Id(),
nullableString: S.String({ nullable: true }),
}),
},
};
const client = new TriplitClient({
schema,
});
await client.insert('test', {
nullableString: null,
});
default
You can provide defaults values or functions for an attribute. Triplit currently support literal values and the following functions:
uuid()
now()
The below schema has literal and function default values.
import { Schema as S } from '@triplit/db';
import { TriplitClient } from '@triplit/client';
const schema = {
messages: {
schema: S.Schema({
id: S.Id(),
text: S.String({ default: 'hello' }),
sent_at: S.Date({ default: S.Default.now() }),
}),
},
};
await client.insert('test', {});
// { id: <uuid>, text: 'hello', sent_at: '2021-03-01T00:00:00.000Z' }
Set
Set types are used to store a collection of non nullable value types. Sets are unordered and do not allow duplicate values.
Lists, which support ordering and duplicate values, are on the roadmap (opens in a new tab).
import { Schema as S } from '@triplit/db';
const stringSet = S.Set(S.String());
Sets support has
and !has
operators in where
statements, which check if the set does or does not contain the value.
Record
Record types support nested information.
import { Schema as S } from '@triplit/db';
const recordType = S.Record({
street: S.String(),
city: S.String(),
state: S.String(),
zip: S.String(),
});
Optional
You may mark attributes in a schema (and record) as optional with the Optional
modifier. They will not be required by the schema and will be undefined at runtime if not provided. Optional attributes may also be deleted and assigned to undefined
in updater functions.
import { Schema as S } from '@triplit/db';
const schema = S.Schema({
address: {
schema: {
street: S.String(),
apartmentNumber: S.Optional(S.String()),
city: S.String(),
state: S.String(),
zip: S.String(),
},
},
});
/*
Will be valid:
{
street: '123 Main St',
city: 'Anytown',
state: 'NY',
zip: '12345',
}
Will also be valid:
{
street: '123 Main St',
city: 'Anytown',
state: 'NY',
zip: '12345',
apartmentNumber: '2',
}
*/
Relationships
To define a relationship between two collections, you define a subquery that describes the relationship with RelationMany
, RelationOne
or RelationById
. A RelationMany
attribute will be in the shape Map<string, Entity>
while RelationOne
and RelationById
are designed for singleton relations and will be directly nested or a sub-object or null
if an applicable entity doesn't exist. Within a relation, either in a where clause or the RelationById
id, parameter, you can reference the current collection's attributes with $
.
import { Schema as S } from '@triplit/db';
const schema = {
departments: {
schema: S.Schema({
id: S.Id(),
name: S.String(),
classes: S.RelationMany('classes', {
where: [['department_id', '=', '$id']],
}),
}),
},
classes: {
schema: S.Schema({
id: S.Id(),
name: S.String(),
level: S.Number(),
building: S.String(),
department_id: S.String(),
department: S.RelationById('department', '$department_id'),
}),
},
};
const query = client
.query('departments')
.where([['classes.building', '=', 'Voter']])
.build();