Implement API pagination for API v2
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Timo Ley 2022-07-21 12:18:25 +02:00
parent e367c341b7
commit c4a8251147
8 changed files with 240 additions and 32 deletions

View File

@ -44,7 +44,7 @@
{
"name": "limit",
"in": "query",
"description": "How many memes should be returned at maximum",
"description": "How many memes should be returned at maximum (-1 for no limit)",
"required": false,
"schema": {
"type": "string"
@ -87,7 +87,7 @@
}
},
"post": {
"summary": "Upload an image or video to JensMemes",
"summary": "Upload an image or video to JensMemes (WIP)",
"security": [
{
"discord": []
@ -239,16 +239,36 @@
}
}
},
"/memes/stream": {
"/memes/count": {
"get": {
"summary": "Returns a stream of new uploaded memes",
"summary": "Gives the total number of memes",
"parameters": [
{
"name": "category",
"in": "query",
"description": "Only count memes from this category ID",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "user",
"in": "query",
"description": "Only count memes from this user",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Stream of memes",
"description": "Amount of memes",
"content": {
"application/x-json-stream": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Meme"
"$ref": "#/components/schemas/Count"
}
}
}
@ -402,6 +422,103 @@
}
}
},
"/users/{id}/memes" : {
"get": {
"summary": "Get all memes of a user",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many memes should be returned at maximum (-1 for no limit)",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "after",
"in": "query",
"description": "ID of the meme after which the returned memes should start",
"required": false,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Meme list response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Meme"
}
}
}
}
},
"default": {
"description": "Some error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/users/{id}/memes/{filename}": {
"get": {
"summary": "Gives a specific meme from a user by filename",
"parameters": [
{
"name": "id",
"in": "path",
"description": "The ID of the user",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "filename",
"in": "path",
"description": "The filename of the meme",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Meme response of this meme",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Meme"
}
}
}
},
"default": {
"description": "Some error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/clips": {
"get": {
"summary": "WIP",
@ -475,12 +592,15 @@
"Meme": {
"type": "object",
"properties": {
"link": {
"filename": {
"type": "string"
},
"id": {
"type": "integer"
},
"ipfs": {
"type": "string"
},
"category": {
"type": "string"
},
@ -517,6 +637,14 @@
}
}
},
"Count": {
"type": "object",
"properties": {
"count": {
"type": "integer"
}
}
},
"Clip": {
"type": "object",
"properties": {

View File

@ -26,6 +26,11 @@ pub struct User {
pub dayuploads: i32,
}
#[derive(Serialize)]
pub struct Count {
pub count: i64,
}
pub enum UserIdentifier {
Id(String),
Token(String),

View File

@ -1,5 +1,5 @@
use crate::ipfs::IPFSFile;
use crate::models::{Category, Meme, MemeOptions, User, UserIdentifier};
use crate::models::{Category, Count, Meme, MemeOptions, User, UserIdentifier};
use crate::JMServiceInner;
use sqlx::mysql::MySqlRow;
use sqlx::{MySqlPool, Result, Row};
@ -46,12 +46,13 @@ impl JMServiceInner {
}
pub async fn get_memes(&self, filter: MemeOptions) -> Result<Vec<Meme>> {
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY memes.id")
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY memes.id LIMIT ?")
.bind(filter.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", filter.username.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.search.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.username.unwrap_or_default()))
.bind(format!("%{}%", filter.search.unwrap_or_default()))
.bind(filter.user_id.unwrap_or_else(|| String::from("%")))
.bind(filter.after.unwrap_or(0))
.bind(filter.limit.unwrap_or(100))
.map(|row: MySqlRow| Meme {
id: row.get("id"),
filename: row.get("filename"),
@ -68,8 +69,8 @@ impl JMServiceInner {
pub async fn get_random_meme(&self, filter: MemeOptions) -> Result<Meme> {
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY RAND() LIMIT 1")
.bind(filter.category.unwrap_or_else(|| String::from("%")))
.bind(format!("%{}%", filter.username.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.search.unwrap_or_else(String::new)))
.bind(format!("%{}%", filter.username.unwrap_or_default()))
.bind(format!("%{}%", filter.search.unwrap_or_default()))
.bind(filter.user_id.unwrap_or_else(|| String::from("%")))
.bind(filter.after.unwrap_or(0))
.map(|row: MySqlRow| Meme {
@ -85,6 +86,20 @@ impl JMServiceInner {
Ok(q)
}
pub async fn count_memes(&self, filter: MemeOptions) -> Result<Count> {
let q: Count = sqlx::query(
"SELECT COUNT(id) AS count FROM memes WHERE category LIKE ? AND user LIKE ?",
)
.bind(filter.category.unwrap_or_else(|| String::from("%")))
.bind(filter.user_id.unwrap_or_else(|| String::from("%")))
.map(|row: MySqlRow| Count {
count: row.get("count"),
})
.fetch_one(&self.db_pool)
.await?;
Ok(q)
}
pub async fn get_user_meme(&self, user_id: String, filename: String) -> Result<Option<Meme>> {
let q: Option<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND memes.user = ? AND filename = ? ORDER BY memes.id DESC")
.bind(user_id)

View File

@ -41,7 +41,7 @@ impl IntoResponse for APIError {
APIError::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)),
APIError::Internal(err) => {
ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err))
}
},
APIError::Service(err) => ErrorResponse::new(
StatusCode::INTERNAL_SERVER_ERROR,
Some(err.get_response_message()),

View File

@ -139,7 +139,7 @@ impl From<MemeFilter> for MemeOptions {
user_id: None,
username: filter.user,
search: filter.search,
limit: None,
limit: Some(-1),
after: None,
}
}

View File

@ -137,7 +137,7 @@ async fn upload(
.to_string();
let file = service.ipfs_add(field.bytes().await?, filename).await?;
files.push(file);
}
},
_ => (),
}
}

View File

@ -1,5 +1,5 @@
use crate::models::{Meme, User};
use serde::Serialize;
use crate::models::{Meme, MemeOptions, User};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct V2Meme {
@ -18,6 +18,15 @@ pub struct V2User {
pub dayuploads: i32,
}
#[derive(Deserialize)]
pub struct MemeFilterQuery {
pub category: Option<String>,
pub user: Option<String>,
pub search: Option<String>,
pub limit: Option<i32>,
pub after: Option<i32>,
}
#[derive(Serialize)]
pub struct CDNEntry {
pub directories: Vec<String>,
@ -52,3 +61,16 @@ impl From<User> for V2User {
}
}
}
impl From<MemeFilterQuery> for MemeOptions {
fn from(query: MemeFilterQuery) -> Self {
Self {
category: query.category,
user_id: query.user,
username: None,
search: query.search,
limit: query.limit,
after: query.after,
}
}
}

View File

@ -12,7 +12,7 @@ use crate::{
JMService,
};
use super::models::{V2Meme, V2User};
use super::models::{MemeFilterQuery, V2Meme, V2User};
async fn get_meme(
Path(meme_id): Path<i32>,
@ -27,11 +27,12 @@ async fn get_meme(
}
async fn get_memes(
Query(filter): Query<MemeFilterQuery>,
Extension(service): Extension<JMService>,
) -> Result<impl IntoResponse, APIError> {
Ok(Json(
service
.get_memes(MemeOptions::empty())
.get_memes(filter.into())
.await?
.into_iter()
.map(V2Meme::from)
@ -39,6 +40,22 @@ async fn get_memes(
))
}
async fn get_random_meme(
Query(filter): Query<MemeFilterQuery>,
Extension(service): Extension<JMService>,
) -> Result<impl IntoResponse, APIError> {
Ok(Json(V2Meme::from(
service.get_random_meme(filter.into()).await?,
)))
}
async fn count_memes(
Query(filter): Query<MemeFilterQuery>,
Extension(service): Extension<JMService>,
) -> Result<impl IntoResponse, APIError> {
Ok(Json(service.count_memes(filter.into()).await?))
}
async fn get_category(
Path(category_id): Path<String>,
Extension(service): Extension<JMService>,
@ -80,6 +97,7 @@ async fn get_users(
}
async fn get_user_memes(
Query(filter): Query<MemeFilterQuery>,
Path(user_id): Path<String>,
Extension(service): Extension<JMService>,
) -> Result<impl IntoResponse, APIError> {
@ -90,8 +108,8 @@ async fn get_user_memes(
user_id: Some(user_id),
username: None,
search: None,
limit: None,
after: None,
limit: filter.limit,
after: filter.after,
})
.await?
.into_iter()
@ -113,15 +131,35 @@ async fn get_user_meme(
)))
}
pub fn routes() -> Router<BoxRoute> {
fn meme_routes() -> Router<BoxRoute> {
Router::new()
.route("/memes", get(get_memes))
.route("/memes/:meme_id", get(get_meme))
.route("/categories", get(get_categories))
.route("/categories/:category_id", get(get_category))
.route("/users", get(get_users))
.route("/users/:user_id", get(get_user))
.route("/users/:user_id/memes", get(get_user_memes))
.route("/users/:user_id/memes/:filename", get(get_user_meme))
.route("/", get(get_memes))
.route("/:meme_id", get(get_meme))
.route("/random", get(get_random_meme))
.route("/count", get(count_memes))
.boxed()
}
fn category_routes() -> Router<BoxRoute> {
Router::new()
.route("/", get(get_categories))
.route("/:category_id", get(get_category))
.boxed()
}
fn user_routes() -> Router<BoxRoute> {
Router::new()
.route("/", get(get_users))
.route("/:user_id", get(get_user))
.route("/:user_id/memes", get(get_user_memes))
.route("/:user_id/memes/:filename", get(get_user_meme))
.boxed()
}
pub fn routes() -> Router<BoxRoute> {
Router::new()
.nest("/memes", meme_routes())
.nest("/categories", category_routes())
.nest("/users", user_routes())
.boxed()
}