Write a schema only app

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 Rust if it hasn't been installed. This bash command is copied from its official site. This is the easiest and recommended way to install Rust.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

When rust is installed, the package manager cargo is in the execution path. Install Teo command line tool with it into the global space.

cargo install teo

Wait for a while and have a rest, cargo is compiling and building Teo. Run this command to verify its installation:

cargo teo --version
Hide output
teo Teo 0.3.2 (Rust1.82) [CLI]

If you see the output which prints the version number, nice job, Teo is successfully installed. If Teo isn't installed successfully and you cannot figure it out, leave us an issue on GitHub. We'll investigate and handle it for you.

Editor plugin

We highly recommend 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

Create an empty directory and create a file named schema.teo.

mkdir hello-teo-schema-only
cd hello-teo-schema-only
touch schema.teo

Paste these content into the file.

connector {
  provider: .mysql,
  url: "mysql://localhost:3306/helloschemaonly",
}
 
server {
  bind: ("0.0.0.0", 5050)
}
 
request middlewares [logRequest]
 
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 a MySQL database.

  • The server block defines how the server behaves. The syntax is the same with connector. 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 port 5050. 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. The fields argument and references 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 language 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.

cargo teo serve
Hide output
[2023-12-20 04:58:40] sqlite connector connected for `main` at "sqlite::memory:"
[2023-12-20 04:58:40] Teo 0.3.2 (Rust1.82, CLI)
[2023-12-20 04:58:40] listening on port 5050

If you see the output like 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@teodev.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
  }
}
Hide HTTP response
{
  "data": {
    "id": 1,
    "email": "ada@teodev.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...
Hide HTTP response
{
  "error": {
    "type": "ValueError",
    "message": "value is invalid",
    "errors": {
      "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
  }
}
Hide HTTP response
{
  "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
      }
    }
  }
}
Hide HTTP response
{
  "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
      }
    }
  }
}
Hide HTTP response
{
  "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
  }
}
Hide HTTP response
{
  "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.