Let's start with the simplest use case: Install Teo and start a schema only app. By the end of this tutorial, you gain understanding of how Teo schema composites and how to write them.
Installation
Install Node.js if it hasn't been installed. There are several ways to install Node.js. You may download the installer from the official website, or install it with tools like NVM. After installation, run this command to verify its installation.
node --version
v21.6.2
If you see some outputs like v21.6.2
, great! Node.js is installed successfully.
Although now you can install Teo globally with the npm
package manager. We highly recommand you to init a Node.js project and install Teo locally. This won't affect other projects that use Teo. To create a directory, init a
Node.js project, and install Teo, follow these commands.
mkdir hello-teo-schema-only
cd hello-teo-schema-only
npm init -y
npm install @teocloud/teo
Use the npx
command to verify whether Teo is installed successfully.
npx teo --version
teo Teo 0.2.4 (Node.js v21.6.2) [CLI]
If you see this output, congratulations! And let's move on.
Editor plugin
We highly recommand you to use VSCode as we created
a plugin for syntax highlight, code diagnostics, auto completion and jump to definition.
Download our plugin from VSCode marketplace.
Write your first schema
In the current working directory, create a file named schema.teo
.
touch schema.teo
Paste these content into the file.
connector {
provider: .sqlite,
url: "sqlite::memory:"
}
server {
bind: ("0.0.0.0", 5050)
}
model User {
@id @autoIncrement @readonly
id: Int
@unique @onSet($if($presents, $isEmail))
email: String
name: String?
@relation(fields: .id, references: .authorId)
posts: Post[]
}
model Post {
@id @autoIncrement @readonly
id: Int
title: String
content: String?
@default(false)
published: Bool
@foreignKey
authorId: Int
@relation(fields: .authorId, references: .id)
author: User
}
Let's explain what this schema describes.
The
connector
block defines to which database this server connects. The content of the block is just a plain object literal with some predefined key value pairs. The expression starts with a.
is enum variant literal in Teo schema. It's inspired by Apple's Swift programming language and is widely used in schema. The benefits of prefixing with a dot is that it's easy to write and
complete. Obviously, this server app connects to an in-memory SQLite database.The
server
block defines how the server behaves. The syntax is the same withconnector
. These blocks are for configuration, and they are called config blocks in Teo. Guess what it does? The answer is that the server will listen at port5050
. The thing wrapped by a pair of parenthesis is tuple type. Teo supports a bunch of builtin types including ranges and tuples. Anyway, there are mostly used in config blocks.The
model
keyword defines models. A model represents a database table and a set of auto synthesized HTTP requests to interact with it. The things start with a@
is called decorator. In Teo schema, plenty of things can be decorated including models, model fields, model relations. A decorator modifies the behavior of the decorated item. The thing start with a$
is called pipeline. There are one or more pipeline items in a pipeline. A pipeline validates and transforms its input value into its output value, or throw errors if value is invalid. Look at this part:$if($presents, $isEmail)
. It means when an email value is set, if the value is not null, which is presented, validate the value against$isEmail
. If the value passed in is not a valid email address, revoke this value and throw an error to the frontend requester. The@relation
decorator defines a model relation. In this case, a user has many posts and a post has an author. Thefields
argument andreferences
argument takes 1 or more primitive fields on the model. These argument describes by which fields the models are connected. The trailing?
in a type expression indicates the value is optional.
In Teo schema, trailing comma is allowed wherever comma is used. The whole schema langauge is inspired by a lot of languages like TypeScript, Swift, GraphQL and Prisma.
When we design this, we want developers to feel familiar. We take advantages from different programming languages, combine them with our descriptive declarations. Teo schema is quite easy and not something to learn with. If you have trouble with understanding Teo schema, take it easy, join our Discord group, there
are people available to help you.
Add models with many-to-many relations
Append these code to the schema file.
model Artist {
@id @autoIncrement @readonly
id: Int
name: String
@relation(through: Perform, local: .artist, foreign: .song)
songs: Song[]
}
model Song {
@id @autoIncrement @readonly
id: Int
name: String
@relation(through: Perform, local: .song, foreign: .artist)
artists: Artist[]
}
@id([.songId, .artistId])
model Perform {
@foreignKey
artistId: Int
@foreignKey
songId: Int
@relation(fields: .songId, references: .id)
song: Song
@relation(fields: .artistId, references: .id)
artist: Artist
}
In Teo, unlike Ruby on Rails or Prisma, join table of a many-to-many relation is declared explicitly. In a many-to-many relation, @relation
takes a join model and two direct relations defined on join model. It's quite clear to write everything out in such a short lines of code. Join tables sometimes is meaningful on their own. With it explicitly declared, database migration won't be hard when you are adding fields and contents to it. Notice the id of the model Perform
is declared at the level of model. This is a compound primary key.
Run the server
Now run this command to start the server.
npx teo serve
[2023-12-20 04:58:40] sqlite connector connected for `main` at "sqlite::memory:"
[2023-12-20 04:58:40] Teo 0.2.4 (Node.js v21.6.2, CLI)
[2023-12-20 04:58:40] listening on port 5050
If you see the output likt above, congratulation, it starts successfully.
If it cannot start successfully, most of the times, the reason is the
database setup and configuration.
Now our server is ready to listen and handle requests.
Create request
Send this request body to /User/create
.
{
"create": {
"email": "ada@teocloud.io",
"name": "Ada",
"posts": {
"create": [
{
"title": "Introducing Teo",
"content": "This post introduces Teo."
},
{
"title": "The next generation framework",
"content": "Use the next generation technology."
}
]
}
},
"include": {
"posts": true
}
}
{
"data": {
"id": 1,
"email": "ada@teocloud.io",
"name": "Ada",
"posts": [
{
"id": 1,
"title": "Introducing Teo",
"content": "This post introduces Teo.",
"published": false,
"authorId": 1
},
{
"id": 2,
"title": "The next generation framework",
"content": "Use the next generation technology.",
"published": false,
"authorId": 1
}
]
}
}
In this request, we've created a user with two related posts. If you request this again, you will get a unique constraint violation error. This is the designated behavior as we've declared email
field of user to be @unique
.
send again...
{
"error": {
"type": "ValueError",
"message": "value is invalid",
"fields": {
"create": "unique value duplicated: email"
}
}
}
Send this request to /Artist/createMany
.
{
"create": [
{
"name": "Ed Sheeran",
"songs": {
"create": [
{
"name": "Perfect"
},
{
"name": "Eyes Closed"
}
]
}
},
{
"name": "Taylor Swift",
"songs": {
"create": [
{
"name": "Love Story"
},
{
"name": "Red"
}
]
}
}
],
"include": {
"songs": true
}
}
{
"data": [
{
"id": 1,
"name": "Ed Sheeran",
"songs": [
{
"id": 1,
"name": "Perfect"
},
{
"id": 2,
"name": "Eyes Closed"
}
]
},
{
"id": 2,
"name": "Taylor Swift",
"songs": [
{
"id": 3,
"name": "Love Story"
},
{
"id": 4,
"name": "Red"
}
]
}
],
"meta": {
"count": 2
}
}
We created two artists with two songs each. In this request, instead of using create
handler, we use createMany
. With createMany
, the arguments become an array of inputs. We can create many database records in a single batch. If any record creation failed, the transaction mechanism is triggered.
Find many request
Let's query what we've created before. Send this request body to /Song/findMany
.
{
"include": {
"artists": {
"select": {
"name": true
}
}
}
}
{
"data": [
{
"id": 1,
"name": "Perfect",
"artists": [
{
"name": "Ed Sheeran"
}
]
},
{
"id": 2,
"name": "Eyes Closed",
"artists": [
{
"name": "Ed Sheeran"
}
]
},
{
"id": 3,
"name": "Love Story",
"artists": [
{
"name": "Taylor Swift"
}
]
},
{
"id": 4,
"name": "Red",
"artists": [
{
"name": "Taylor Swift"
}
]
}
],
"meta": {
"count": 4
}
}
We limited the returned value with select
. Only selected fields are returned. We can query further by specify some where
conditions.
{
"where": {
"name": {
"startsWith": "Perfect"
}
},
"include": {
"artists": {
"where": {
"name": "Not exist"
},
"select": {
"name": true
}
}
}
}
{
"data": [
{
"id": 1,
"name": "Perfect",
"artists": []
}
],
"meta": {
"count": 1
}
}
In this request, only 1 song is returned. Although we include the artists in the response, the only artist that is related to the song is filtered out by a where condition.
Update requests
To perform a update request, combine where
and update
together in the request body and send it to /Song/update
.
{
"where": {
"id": 1
},
"update": {
"name": "Perfect Symphony",
"artists": {
"update": {
"where": {
"id": 1
},
"update": {
"name": "Richard Clayderman"
}
}
}
},
"include": {
"artists": true
}
}
{
"data": {
"id": 1,
"name": "Perfect Symphony",
"artists": [
{
"id": 1,
"name": "Richard Clayderman"
}
]
}
}
We can nested update related records with Teo. If any update is invalid, it rollbacks.
Summary
There are a lot of builtin model route handlers including delete
, upsert
, copy
, createMany
, findFirst
, findUnique
and so on. Relation queries are supported except delete
and deleteMany
.
Hope it's not hard to catch up with us. Teo is quite easy and simple to use. Find us in our Discord group if you have any troubles.
In the next tutorial in this series, you'll learn how to write route handlers with Teo. Programming code is necessary to write custom business logics.
Top comments (0)