add cursor and move to uuidv7
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Hamcha 2023-07-13 17:43:05 +02:00
parent 203df76d6c
commit 81134dc0dd
Signed by: hamcha
GPG Key ID: 1669C533B8CF6D89
16 changed files with 435 additions and 145 deletions

6
.cargo/config.toml Normal file
View File

@ -0,0 +1,6 @@
[build]
rustflags = ["--cfg", "uuid_unstable"]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=mold", "--cfg", "uuid_unstable"]

6
Cargo.lock generated
View File

@ -1830,12 +1830,12 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.3.4"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
dependencies = [
"atomic",
"getrandom",
"rand",
"serde",
]

View File

@ -12,7 +12,7 @@ tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.28", features = ["full"] }
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate", "json", "offline"] }
uuid = { version = "1.3", features = ["v4", "fast-rng", "serde"] }
uuid = { version = "1.4", features = ["v7", "serde", "std"] }
serde = { version = "1" }
serde_json = { version = "1", features = ["raw_value"] }
figment = { version = "0.10", features = ["toml", "env", "test"] }

View File

@ -1,7 +1,7 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- noqa: RF05
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
name VARCHAR UNIQUE NOT NULL,
email VARCHAR,
password VARCHAR,
@ -14,7 +14,7 @@ CREATE TABLE users (
);
CREATE TABLE sites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
owner UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
name VARCHAR UNIQUE NOT NULL,
title VARCHAR NOT NULL,
@ -25,7 +25,7 @@ CREATE TABLE sites (
);
CREATE TABLE pages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
site UUID NOT NULL REFERENCES sites (id) ON DELETE CASCADE,
author UUID REFERENCES users (id) ON DELETE SET NULL,
slug VARCHAR NOT NULL,

View File

@ -1,7 +1,7 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- noqa: RF05
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
actor UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
secret VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),

View File

@ -1,7 +1,7 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- noqa: RF05
CREATE TABLE collections (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
id UUID PRIMARY KEY,
site UUID NOT NULL REFERENCES sites (id) ON DELETE CASCADE,
slug VARCHAR NOT NULL,
name VARCHAR NOT NULL,

View File

@ -1,4 +1,4 @@
-- Remove constraint and index (collections_site_slug_unique, collections_site_slug_idx)
-- Remove constraint and index
ALTER TABLE collections DROP CONSTRAINT collections_site_slug_unique;
DROP INDEX IF EXISTS collections_site_slug_unique;

View File

@ -1,28 +1,5 @@
{
"db": "PostgreSQL",
"0e89d76bdba2178afc2fb2c2a5eba0404e68e7a1d26d9fd5f1161f055dad92df": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Timestamp",
"Timestamp"
]
}
},
"query": "INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id"
},
"11e96cfd8c2736f13ce55975ea910dd68640f6f14e38a4b3342d514804e3de27": {
"describe": {
"columns": [],
@ -35,20 +12,228 @@
},
"query": "DELETE FROM sessions WHERE id = $1"
},
"244e021e36237f5e9ce0f17b6eb3ba58d42fe292e7517a33938c5191bc8787e1": {
"23f1a1f2aa96a5a23880f330d04f5dc695a148f4aa50b8c76378b68944569e44": {
"describe": {
"columns": [
{
"name": "author_display_name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published\n FROM pages \n JOIN\n sites ON site = sites.id\n JOIN\n users ON author = users.id\n WHERE\n slug = $1\n AND sites.name = $2 \n AND pages.deleted_at IS NULL\n AND sites.deleted_at IS NULL"
},
"28ec2833283df836e457161f44f0fbc75ed37dcc0ba63eb09bf285aae2f7a380": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Varchar",
"Varchar"
]
}
},
"query": "INSERT INTO collections (id, site, slug, name)\n VALUES ($1, $2, $3, $4)"
},
"29e0259def5c6fdc34c6a18345150d2714736b91c1f3040fcb07d77cbe2552e1": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "owner",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "owner_display_name",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 5,
"type_info": "Timestamp"
},
{
"name": "collections!: Vec<CollectionNameAndSlug>",
"ordinal": 6,
"type_info": "RecordArray"
}
],
"nullable": [
false,
false,
null,
false,
true,
false,
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "INSERT INTO sites (name, owner, title, description) VALUES ($1, $2, $3, $4)"
"query": "SELECT\n sites.name,\n users.name as owner,\n COALESCE(users.display_name,users.name) as owner_display_name,\n title,\n description,\n sites.created_at,\n array_agg(row(collections.slug, collections.name)) as \"collections!: Vec<CollectionNameAndSlug>\"\n FROM sites\n JOIN\n users ON sites.owner = users.id\n JOIN\n collections ON collections.site = sites.id\n WHERE\n sites.name = $1\n AND sites.deleted_at IS NULL\n GROUP BY\n sites.name, users.name, users.display_name, title, description, sites.created_at"
},
"320625aa3fac73cb43d6a68592767174558ab1a6923d3574ad629d1a1bd0d4a6": {
"describe": {
"columns": [
{
"name": "author_display_name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "author_username",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "slug",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "tags",
"ordinal": 5,
"type_info": "VarcharArray"
},
{
"name": "blocks!: Json<Vec<PostBlock>>",
"ordinal": 6,
"type_info": "Jsonb"
},
{
"name": "created_at",
"ordinal": 7,
"type_info": "Timestamp"
},
{
"name": "modified_at",
"ordinal": 8,
"type_info": "Timestamp"
},
{
"name": "published",
"ordinal": 9,
"type_info": "Bool"
}
],
"nullable": [
null,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Timestamp",
"Int8"
]
}
},
"query": "SELECT\n COALESCE(users.display_name,users.name) as author_display_name,\n users.name as author_username,\n pages.slug,\n pages.title,\n pages.description,\n pages.tags,\n pages.blocks as \"blocks!: Json<Vec<PostBlock>>\",\n pages.created_at,\n pages.modified_at,\n pages.published\n FROM pages\n JOIN\n users ON author = users.id\n WHERE\n site = $1\n AND $2 = ANY(collections)\n AND pages.deleted_at IS NULL\n AND published = true\n AND pages.created_at <= $3\n ORDER BY created_at DESC\n LIMIT $4\n "
},
"35deaad55b84124dcba02c555718fe815003e3f50b1c8ee2fc7e540009fa5aef": {
"describe": {
@ -68,23 +253,50 @@
},
"query": "SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
},
"51786c014e6f0863b0462646121bed527cc5c0bac343974f2d3b049e46c14e72": {
"39237d1e40ce33ce386320212a53986cb21949dd07014772a150242caf4fa610": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Text",
"VarcharArray",
"Jsonb",
"Uuid",
"Uuid",
"Text"
"UuidArray",
"Bool"
]
}
},
"query": "UPDATE pages SET title = COALESCE($1, title), description = COALESCE($2, description), tags = COALESCE($3, tags), blocks = COALESCE($4, blocks) WHERE author = $5 AND site = $6 AND slug = $7 AND deleted_at IS NULL"
"query": "INSERT INTO pages (id, author, site, slug, title, description, tags, blocks, collections, published)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
},
"562254387ad52ad5ee3cf806085873485d6b8a0b372ac59a0796b9f0c8910cf2": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Timestamp",
"Timestamp"
]
}
},
"query": "INSERT INTO sessions (id, actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4, $5) RETURNING id"
},
"61fcf083e6abcfed9480164f3ed4ff2f396c8041a30df002f6e4af920b41f4e4": {
"describe": {
@ -111,24 +323,6 @@
},
"query": "DELETE FROM sessions WHERE expires_at < $1"
},
"7b294c850a2feba882b52cc479ae943fdd4a3d26278274ea19eb6a3a0393b4ad": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Varchar",
"Varchar",
"Text",
"VarcharArray",
"Jsonb"
]
}
},
"query": "INSERT INTO pages (author, site, slug, title, description, tags, blocks) VALUES ($1, $2, $3, $4, $5, $6, $7)"
},
"7bdb55b060b5e81b50c1bf86cb117215cba7010c91f8384a397efac06a88507d": {
"describe": {
"columns": [],
@ -143,6 +337,35 @@
},
"query": "DELETE FROM pages WHERE author = $1 AND site = $2 AND slug = $3"
},
"82450bdd6a4a7ac9ee66fb98fd87993b70fa58577876b0761ceac9e31af27b8a": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
},
{
"name": "created_at",
"ordinal": 1,
"type_info": "Timestamp"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"UuidArray"
]
}
},
"query": "INSERT INTO users ( id, name, password, roles )\n VALUES ( $1,$2,$3,$4 ) RETURNING id, created_at"
},
"9f37c6f3929ca1bf2a6ec55291d652b216419fd13e27b3d61d23c9cf900c501e": {
"describe": {
"columns": [
@ -164,66 +387,6 @@
},
"query": "UPDATE sessions SET expires_at = $1 WHERE id = $2 RETURNING id"
},
"c04874a0f4ab7409f53ee50ec34679f6b274b350aff39a8710575c54fcaef1e4": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Uuid"
]
}
},
"query": "UPDATE sites SET name = COALESCE($1, name), title = COALESCE($2, title), description = COALESCE($3, description), modified_at = NOW() WHERE name = $4 AND owner = $5 AND deleted_at IS NULL"
},
"c1121a7da8dc099e6eb3ed8d1008a247524f7b953f2d0db674371714f09c6530": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "owner",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "title",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at",
"ordinal": 4,
"type_info": "Timestamp"
}
],
"nullable": [
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT sites.name, users.name as owner, title, description, sites.created_at FROM sites JOIN users ON sites.owner = users.id WHERE sites.name = $1 AND sites.deleted_at IS NULL"
},
"c567d96c92638902bc90f5fc0aa5bbbe0b6d26607767720a87fdbe67335a0180": {
"describe": {
"columns": [
@ -335,6 +498,22 @@
},
"query": "DELETE FROM sites WHERE name = $1 AND owner = $2"
},
"d0005084c0ae3939460c3c727b29899c65a05e53fbc477cdda9665e7f243d5e9": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Uuid"
]
}
},
"query": "UPDATE sites SET\n name = COALESCE($1, name),\n title = COALESCE($2, title),\n description = COALESCE($3, description),\n modified_at = NOW()\n WHERE\n name = $4\n AND owner = $5\n AND deleted_at IS NULL"
},
"d9649c375db5825adda3b91c52468235fd9c361b827df64350c7e65bca0f00b4": {
"describe": {
"columns": [],
@ -349,33 +528,49 @@
},
"query": "UPDATE pages SET deleted_at = NOW() WHERE author = $1 AND site = $2 AND slug = $3 AND deleted_at IS NULL"
},
"f054746ab3092a8671604c9388e02220d62d337e9d199fa9102c0363a72f6940": {
"e9f521d288a92f1e70b4de35b0d75792007d910ee65e2782eee4a3cdd30560a0": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
},
{
"name": "created_at",
"ordinal": 1,
"type_info": "Timestamp"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Uuid",
"Varchar",
"UuidArray"
"Text"
]
}
},
"query": "INSERT INTO users ( name, password, roles ) VALUES ( $1,$2,$3 ) RETURNING id, created_at"
"query": "INSERT INTO sites (id, name, owner, title, description)\n VALUES ($1, $2, $3, $4, $5) RETURNING id"
},
"ecbbf6f400e058cd6e8695354d65a1883f974a39e11ac5ba0f7fba408fcf6b2a": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"VarcharArray",
"Jsonb",
"Uuid",
"Uuid",
"Text",
"UuidArray",
"Bool"
]
}
},
"query": "UPDATE pages SET title = COALESCE($1, title), description = COALESCE($2, description), tags = COALESCE($3, tags), blocks = COALESCE($4, blocks), collections = COALESCE($8, collections), published = COALESCE($9, published) WHERE author = $5 AND site = $6 AND slug = $7 AND deleted_at IS NULL"
},
"f3d0f52ab3c9d7ed46086d21cd88207a75d9f2e2990f3fb721f0e14e1ec52e10": {
"describe": {

View File

@ -38,7 +38,8 @@ impl Session {
let expires = now + duration;
let secret = random();
let result = sqlx::query!(
"INSERT INTO sessions (actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4) RETURNING id",
"INSERT INTO sessions (id, actor, secret, created_at, expires_at) VALUES ($1, $2, $3, $4, $5) RETURNING id",
Uuid::now_v7(),
user_id,
secret,
now,

View File

@ -60,13 +60,15 @@ impl User {
roles: &Vec<Uuid>,
) -> Result<Self> {
let result = sqlx::query!(
r#"INSERT INTO users ( name, password, roles ) VALUES ( $1,$2,$3 ) RETURNING id, created_at"#,
username,
hash(&password)?,
roles,
)
.fetch_one(pool)
.await?;
r#"INSERT INTO users ( id, name, password, roles )
VALUES ( $1,$2,$3,$4 ) RETURNING id, created_at"#,
Uuid::now_v7(),
username,
hash(&password)?,
roles,
)
.fetch_one(pool)
.await?;
Ok(Self {
id: result.id,
name: username.to_owned(),

View File

@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
use sqlx::{types::Json, FromRow};
use uuid::Uuid;
use crate::cursor::{AsCursor, CursorList};
use super::Error;
#[derive(Deserialize, Serialize, FromRow)]
@ -115,15 +117,29 @@ pub struct PostWithAuthor {
pub blocks: Json<Vec<PostBlock>>,
pub created_at: NaiveDateTime,
pub modified_at: Option<NaiveDateTime>,
pub deleted_at: Option<NaiveDateTime>,
pub published: bool,
}
impl AsCursor<NaiveDateTime> for PostWithAuthor {
fn cursor(&self) -> NaiveDateTime {
self.created_at
}
}
#[async_trait]
pub trait PostRepository {
/// Get a post from its slug
async fn get_post_from_url(&self, site: &str, slug: &str) -> Result<PostWithAuthor, Error>;
/// List posts in a collection
async fn list_posts_in_collection(
&self,
site: &Uuid,
collection: &Uuid,
cursor: Option<NaiveDateTime>,
limit: i64,
) -> Result<CursorList<PostWithAuthor, NaiveDateTime>, Error>;
/// Create a new post
async fn create_post(
&self,

17
src/cursor.rs Normal file
View File

@ -0,0 +1,17 @@
pub struct CursorList<T, C> {
pub items: Vec<T>,
pub next_cursor: Option<C>,
}
pub trait AsCursor<C> {
fn cursor(&self) -> C;
}
impl<T: AsCursor<C>, C> CursorList<T, C> {
pub fn new(items: Vec<T>, limit: usize) -> Self {
CursorList {
next_cursor: items.get(limit - 1).map(|x| x.cursor()),
items,
}
}
}

View File

@ -15,7 +15,9 @@ impl CollectionRepository for Database {
for (slug, name) in DEFAULT_COLLECTIONS {
sqlx::query!(
r#"INSERT INTO collections (site, slug, name) VALUES ($1, $2, $3)"#,
r#"INSERT INTO collections (id, site, slug, name)
VALUES ($1, $2, $3, $4)"#,
Uuid::now_v7(),
site,
slug,
name

View File

@ -1,19 +1,66 @@
use async_trait::async_trait;
use chrono::NaiveDateTime;
use content::post::PostBlock;
use serde_json::json;
use sqlx::types::Json;
use uuid::Uuid;
use crate::content::{
self,
post::{CreatePostData, PostRepository, PostWithAuthor, UpdatePostData},
Error,
use crate::{
content::{
self,
post::{CreatePostData, PostRepository, PostWithAuthor, UpdatePostData},
Error,
},
cursor::CursorList,
};
use super::Database;
#[async_trait]
impl PostRepository for Database {
async fn list_posts_in_collection(
&self,
site: &Uuid,
collection: &Uuid,
from: Option<NaiveDateTime>,
limit: i64,
) -> Result<CursorList<PostWithAuthor, NaiveDateTime>, Error> {
let posts = sqlx::query_as!(
PostWithAuthor,
r#"SELECT
COALESCE(users.display_name,users.name) as author_display_name,
users.name as author_username,
pages.slug,
pages.title,
pages.description,
pages.tags,
pages.blocks as "blocks!: Json<Vec<PostBlock>>",
pages.created_at,
pages.modified_at,
pages.published
FROM pages
JOIN
users ON author = users.id
WHERE
site = $1
AND $2 = ANY(collections)
AND pages.deleted_at IS NULL
AND published = true
AND pages.created_at <= $3
ORDER BY created_at DESC
LIMIT $4
"#,
site,
collection,
from.unwrap_or_else(|| NaiveDateTime::MAX),
limit
)
.fetch_all(&self.pool)
.await?;
Ok(CursorList::new(posts, limit as usize))
}
async fn get_post_from_url(
&self,
site: &str,
@ -31,7 +78,6 @@ impl PostRepository for Database {
pages.blocks as "blocks!: Json<Vec<PostBlock>>",
pages.created_at,
pages.modified_at,
pages.deleted_at,
pages.published
FROM pages
JOIN
@ -62,7 +108,9 @@ impl PostRepository for Database {
data: CreatePostData,
) -> Result<(), Error> {
sqlx::query!(
"INSERT INTO pages (author, site, slug, title, description, tags, blocks, collections, published) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
"INSERT INTO pages (id, author, site, slug, title, description, tags, blocks, collections, published)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
Uuid::now_v7(),
owner,
site,
data.slug,

View File

@ -69,7 +69,9 @@ impl SiteRepository for Database {
async fn create_site(&self, owner: &Uuid, options: CreateSiteData) -> Result<Uuid, Error> {
let result = sqlx::query!(
"INSERT INTO sites (name, owner, title, description) VALUES ($1, $2, $3, $4) RETURNING id",
"INSERT INTO sites (id, name, owner, title, description)
VALUES ($1, $2, $3, $4, $5) RETURNING id",
Uuid::now_v7(),
options.name,
owner,
options.title,

View File

@ -1,6 +1,7 @@
mod auth;
mod builtins;
mod content;
mod cursor;
mod database;
mod http;
mod routes;