Перейти к содержанию

Создайте CRUD API с помощью rust

Posted on:2 марта 2023 г. at 08:55

Example Dynamic OG Image link

Привет. Сегодня я собираюсь показать вам, как создать очень простой REST API в Rust.

Для этого мы собираемся использовать Teo фрамеворк. Teo - это первый доминирующий веб-фреймворк для rust, подобный Ruby on Rails для Ruby и Django для Python. Эта статья будет охватывать все вещи, упомянутые ниже:

Установка Teo

Если у вас не установлен rust, установите его с официального сайта. Как только rust будет установлен, давайте установим инструмент командной строки Teo.

Мы устанавливаем Teo с помощью cargo, инструмента управления пакетами rust.

cargo install teo

После установки Teo введите cargo teo --help для подтверждения установки.

Создание проекта

Давайте создадим пустой проект с помощью mkdir и создадим в нем наш первый файл схемы.

mkdir blog-example
cd blog-example
touch schema.teo

Задекларируем коннектор с SQL

Коннектор указывает, какой тип базы данных он использует и к какой базе данных он подключен. Один проект Teo может иметь только один коннектор. Давайте объявим это в схеме.

connector {
  provider .sqlite
  url "sqlite::memory:"
}

Этот коннектор подключается к базе данных SQLite в памяти. Это отлично подходит для демонстрационных целей. Teo поддерживает также MySQL, PostgreSQL и MongoDB. В процессе производства вы можете захотеть выбрать один из них. Просто измените коннектор, и вам даже не нужно менять свои модели, если вы не используете MongoDB.

Конфигурация сервера

У Teo отличный язык схем, и внутри него все объявлено. Как и конфигурация сервера. Добавьте эти строки в конец schema.teo.

server {
  bind ("0.0.0.0", 5500)
  jwtSecret "someTopSecret"
}

Наш сервер будет прослушивать порт 5500.

Клиентские запросы

Иметь фронтенд-разработчика для копирования всех объектов передачи данных и интерфейсов не так уж элегантно. Поскольку эти работы настолько дублируются. Teo создает типобезопасные клиенты запросов для разработчиков интерфейсов. В этом руководстве давайте объявим клиент TypeScript.

client ts {
  provider .typeScript
  dest "../blog-example-client/"
  package true
  host "http://127.0.0.1:5500"
  gitCommit true
}

Обратите внимание на элемент host, клиент запросов будет отправлять запросы только на наш работающий сервер на порту 5500. Мы сгенерируем наш клиент запроса после объявления моделей.

Модели

Объявлять модели в Teo просто. Обработчики HTTP-маршрутов автоматически генерируются в соответствии с моделями.

model User {
  @id @readonly @autoIncrement
  id: Int
  @unique @onSet($isEmail)
  email: String
  name: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
}

model Post {
  @id @readonly @autoIncrement
  id: Int
  title: String
  content: String?
  @default(false)
  published: Bool
  @foreignKey
  userId: Int
  @relation(fields: .userId, references: .id)
  user: User
}

Мы объявили две модели: User и Post. Электронная почта пользователя уникальна. Когда задано значение электронной почты, выполняется проверка. У пользователя много записей, в то время как у записи есть пользователь. Сообщение имеет необязательный контент, опубликовано имеет значение по умолчанию false.

Генерация запросов клиента и отправка

Давайте сгенерируем клиент запроса с помощью следующей команды и запустим сервер:

cargo teo generate client
cargo teo serve

Перейдите в каталог сгенерированного клиента запросов и установите там ts-node.

Давайте напишем несколько скриптов для отправки запросов на наш сервер. Создайте файл с именем try.ts со следующим содержимым внутри сгенерированного каталога клиента запроса:

import { teo } from "./src"

async function main() {
  const results = await teo.user.create({
    create: {
      email: "peter@teocloud.io",
      name: "Peter",
      posts: {
        create: [
          {
            title: "First post",
            content: "First post has a content",
          },
          {
            title: "Second post",
            content: "Second post is published",
            published: true,
          },
        ],
      },
    },
    include: {
      posts: true,
    },
  })
  console.log(JSON.stringify(results, null, 2))
}

main()

Давайте запустим этот файл, и вы увидите следующие выходные данные:

npx ts-node try.ts
{
  "data": {
    "id": 1,
    "email": "peter@teocloud.io",
    "name": "Peter",
    "posts": [
      {
        "id": 1,
        "title": "First post",
        "content": "First post has a content",
        "published": false,
        "userId": 1
      },
      {
        "id": 2,
        "title": "Second post",
        "content": "Second post is published",
        "published": true,
        "userId": 1
      }
    ]
  }
}

Здесь мы создали пользователя с двумя постами. На стороне сервера вы увидите строку вывода, подобную этой:

2023-02-28 14:23:11.163375 +08:00 create on User - 200 5ms

Этот запрос занимает 5 мс на моем Mac. Это очень быстро.

Сеанс пользователя

API не завершен, если пользователь не может войти в систему. Давайте добавим пользовательский сеанс, изменив объявление пользовательской модели следующим образом:

@identity
model User {
  @id @readonly @autoIncrement
  id: Int
  @unique @onSet($isEmail) @identity
  email: String
  name: String
  @writeonly @onSet($hasLength(8...16).isSecurePassword.bcryptSalt)
  @identityChecker($bcryptVerify($self.get(.password)))
  password: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
}

Мы добавили декоратор @identity в пользовательскую модель, это делает пользовательскую модель аутентифицируемой. Также добавили этот декоратор в поле электронной почты. Это означает, что пользователи входят в систему с помощью электронной почты. Мы добавили новое поле - пароль. Это доступно только для записи, и интерфейс не сможет cчитать. Сохраняем зашифрованный пароль и проверяем пароль пользователя при входе в систему.

Давайте выключим сервер, восстановим клиент и перезапустим сервер.

cargo teo generate client
cargo teo serve

Теперь давайте запустим следующий скрипт:

import { teo } from "./src"

async function main() {
  const _peter = (await teo.user.create({
    create: {
      email: "peter@teocloud.io",
      name: "Peter",
      password: "Peter$12345"
    },
  })).data
  const peterSignIn = await teo.user.signIn({
    credentials: {
      email: "peter@teocloud.io",
      password: "Peter$12345"
    }
  })
  console.log(JSON.stringify(peterSignIn, null, 2))
}

main()

Вы получите информацию о пользователе вместе с токеном JWT, подобным этому.

{
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjoiVXNlciIsImV4cCI6MTcwOTEwMTk4Nn0.Twv7-nAmqyKfgM2iU9QBSNSqb02lm0HUQy80bTyaR_k"
  },
  "data": {
    "id": 1,
    "email": "peter@teocloud.io",
    "name": "Peter"
  }
}

Защита API

Теперь у нас есть пользовательские сеансы, давайте защитим наш API с помощью guards. Средства защиты не позволяют произвольным лицам получать доступ к защищенным ресурсам API. Это делает наш API безопасным.

Замените два объявления модели следующими:

@canMutate(
  $when(.update, $identity($is($self)))
  .when(.delete, $identity($is($self)))
)
@identity
model User {
  @id @readonly @autoIncrement
  id: Int
  @unique @onSet($isEmail) @identity
  email: String
  name: String
  @writeonly @onSet($hasLength(8...16).isSecurePassword.bcryptSalt)
  @identityChecker($bcryptVerify($self.get(.password)))
  password: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
}

@canMutate(
  $when(.update, $identity($is($self, .user)))
  .when(.delete, $identity($is($self, .user)))
  .when(.create, $identity($isA(User)))
)
model Post {
  @id @readonly @autoIncrement
  id: Int
  title: String
  content: String?
  @default(false)
  published: Bool
  @foreignKey
  userId: Int
  @relation(fields: .userId, references: .id)
  user: User
}

Мы добавили декоратор @canMutate к каждой из моделей. Что касается пользователя, то любой может создать пользователя, он же зарегистрироваться. Но только сам пользователь может обновить или удалить себя. Только владелец записи может обновить или удалить запись. Создавать сообщения могут только действительные пользователи платформы.

Опять же, давайте выключим сервер, восстановим клиент и перезапустим сервер.

cargo teo generate client
cargo teo serve

Запустите этот скрипт, чтобы на этот раз проверить наших охранников.

import { teo } from "./src"

async function main() {
  // peter
  const peter = (await teo.user.create({
    create: {
      email: "peter@teocloud.io",
      name: "Peter",
      password: "Peter$12345"
    },
  })).data
  const peterToken = (await teo.user.signIn({
    credentials: {
      email: "peter@teocloud.io",
      password: "Peter$12345"
    }
  })).meta.token

  // ada
  const _ada = (await teo.user.create({
    create: {
      email: "ada@teocloud.io",
      name: "Ada",
      password: "Ada$12345"
    },
  })).data
  const adaToken = (await teo.user.signIn({
    credentials: {
      email: "ada@teocloud.io",
      password: "Ada$12345"
    }
  })).meta.token
  const peterPost = (await teo.$withToken(peterToken).post.create({
    create: {
      title: "Post 1",
      content: null,
      user: {
        connect: {
          id: peter.id
        }
      }
    },
  })).data
  console.log("peter created:", peterPost)
  const peterUpdatedPost = await teo.$withToken(peterToken).post.update({
    where: {
      id: peterPost.id,
    },
    update: {
      content: "Peter wrote some content."
    }
  })
  console.log("peter updated:", peterUpdatedPost)
  try {
    const _adaUpdatePeterPost = await teo.$withToken(adaToken).post.update({
      where: {
        id: peterPost.id,
      },
      update: {
        content: "Ada changed some content."
      }
    })
  } catch(err) {
    console.log(err)
  }
}

main()

Вы увидите результаты, подобные этому:

peter created: { id: 1, title: 'Post 1', published: false, userId: 1 }
peter updated: {
  data: {
    id: 1,
    title: 'Post 1',
    content: 'Peter wrote some content.',
    published: false,
    userId: 1
  }
}
TeoError: Permission denied.
    at request (/Users/victor/Developer/blog-example-client/src/index.js:88:13)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async main (/Users/victor/Developer/blog-example-client/try3.ts:55:33) {
  type: 'PermissionError',
  errors: { update: 'permission denied' }
}

Питер может создавать и обновлять свои посты, в то время как, если Ада попытается обновить пост Питера, клиент запроса выдает ошибку “Отказано в разрешении”. Guards делают API безопасным, и написать guards в Teo не так сложно, как в других фреймворках.

Сокращение выходные данные при нахождении многих записей

Поскольку наше приложение представляет собой систему ведения блога, при перечислении сообщений пользователя результаты будут довольно большими. Давайте сократим его с помощью элемента конвейера Teo $when. $if тестируешь условия и делаешь что-то условно.

@onOutput($when(.many, $if($exists, then: $ellipsis("...", 5))))
content: String?

При нахождении одного поста ответ по-прежнему заполнен. При отображении в виде списка результатов в нем отображается не более 5 слов.

Давайте снова восстановим клиент и перезапустим сервер:

cargo teo generate client
cargo teo serve

Запустите этот скрипт:

import { teo } from "./src"

async function main() {
  const results = await teo.user.create({
    create: {
      email: "peter@teocloud.io",
      name: "Peter",
      password: "Peter$12345",
      posts: {
        create: [
          {
            title: "First post",
            content: null,
          },
          {
            title: "Second post",
            content: "Second post is published",
            published: true,
          },
        ],
      },
    },
    include: {
      posts: true,
    },
  })
  console.log("with user:", JSON.stringify(results, null, 2))
  const post1 = await teo.post.findUnique({
    where: {
      id: 1
    }
  });
  console.log("post 1:", JSON.stringify(post1, null, 2))
  const post2 = await teo.post.findUnique({
    where: {
      id: 2
    }
  });
  console.log("post 2:", JSON.stringify(post2, null, 2))
}

main()

Вы увидите следующие выходные данные:

with user: {
  "data": {
    "id": 1,
    "email": "peter@teocloud.io",
    "name": "Peter",
    "posts": [
      {
        "id": 1,
        "title": "First post",
        "published": false,
        "userId": 1
      },
      {
        "id": 2,
        "title": "Second post",
        "content": "Secon...",
        "published": true,
        "userId": 1
      }
    ]
  }
}
post 1: {
  "data": {
    "id": 1,
    "title": "First post",
    "published": false,
    "userId": 1
  }
}
post 2: {
  "data": {
    "id": 2,
    "title": "Second post",
    "content": "Second post is published",
    "published": true,
    "userId": 1
  }
}

Содержимое второго поста отображается в результатах списка, в нем отображаются только 5 слов. Когда оно только извлекается, содержимое заполнено.

Расширение с помощью объектов модели

Опыт Тео интересен. До сих пор мы на самом деле не написали ни строчки Rust. Однако язык Teo schema language не может предоставить все функции, необходимые разработчику. Teo позволяет генерировать объекты модели. Использование объектов модели аналогично использованию объекта модели в любой другой ORM.

Давайте обновим наш проект до “настоящего” проекта Rust. Запустите эту команду прямо в каталоге, где находится schema.teo.

cargo init --bin

Добавьте эти две строки в раздел [dependencies] Cargo.toml.

teo = { version = "0.0.49" }
tokio = { version = "1.25.0", features = ["macros"] }

Давайте объявим блок сущности модели внутри файла схемы.

entity rs {
  provider .rust
  dest "./src/entities"
}

Затем мы сгенерируем файлы сущностей с помощью этой команды генератора:

cargo teo generate entity

Для простоты давайте распечатаем пользователя, когда он будет сохранен. Замените содержимое src/main.rs на это:

mod entities;

use teo::prelude::{main, AppBuilder};
use self::entities::user::User;

#[main]
async fn main() -> std::io::Result<()> {
    let mut app_builder = AppBuilder::new();
    app_builder.callback("printUser", |user: User| async move {
        println!("{}", user);
    });
    let app = app_builder.build().await;
    app.run().await
}

Мы создали обратный вызов с именем printUser, давайте подключим это к схеме. Поместите этот декоратор поверх блока пользовательской модели.

@beforeSave($self.callback("printUser"))

Так что весь пользовательский блок станет таким:

@beforeSave($self.callback("printUser"))
@canMutate(
  $when(.update, $identity($is($self)))
  .when(.delete, $identity($is($self)))
)
@identity
model User {
  @id @readonly @autoIncrement
  id: Int
  @unique @onSet($isEmail) @identity
  email: String
  name: String
  @writeonly @onSet($hasLength(8...16).isSecurePassword.bcryptSalt)
  @identityChecker($bcryptVerify($self.get(.password)))
  password: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
}

Теперь вместо того, чтобы запускать cargo teo serve, мы запускаем cargo run serve. На этот раз мы создаем наш собственный исполняемый файл вместо использования CLI Teo по умолчанию. Этот исполняемый файл используется так же, как и в Teo CLI. Разница в том, что он считывает пользовательский программный код и компилирует его вместе с базовой функциональностью Teo.

Запустите этот скрипт из клиента запросов:

import { teo } from "./src"

async function main() {
  const results = await teo.user.create({
    create: {
      email: "peter@teocloud.io",
      name: "Peter",
      password: "Peter$12345",
    },
  })
  console.log(JSON.stringify(results, null, 2))
}

main()

В консоли сервера вы увидите, что этого пользователя.

User { 
  id: Null, 
  email: String("peter@teocloud.io"), 
  name: String("Peter"), 
  password: String("$2b$12$4DdMfA1DQN8J0w10LBulT.eKHOpOgkrnFAGouohqjWHfqSTZR4mF.") 
}