Implement API pagination for API v2
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
e367c341b7
commit
c4a8251147
8 changed files with 240 additions and 32 deletions
144
spec/v2.json
144
spec/v2.json
|
@ -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": {
|
||||
|
|
|
@ -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),
|
||||
|
|
27
src/sql.rs
27
src/sql.rs
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ async fn upload(
|
|||
.to_string();
|
||||
let file = service.ipfs_add(field.bytes().await?, filename).await?;
|
||||
files.push(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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue