feat: implement repo API search in REST API
This commit is contained in:
parent
2755e346d7
commit
5ed1ebacef
6 changed files with 315 additions and 1 deletions
|
@ -15,3 +15,6 @@ license = "AGPLv3 or later version"
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
version = "1"
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.db-core]
|
||||||
|
path = "../db/db-core"
|
||||||
|
|
|
@ -17,17 +17,33 @@
|
||||||
*/
|
*/
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use db_core::Repository;
|
||||||
|
|
||||||
pub const ROUTES: Api = Api::new();
|
pub const ROUTES: Api = Api::new();
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Search {
|
||||||
|
pub repository: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Search {
|
||||||
|
const fn new() -> Search {
|
||||||
|
let repository = "/api/v1/search/repository";
|
||||||
|
Search { repository }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Api {
|
pub struct Api {
|
||||||
pub get_latest: &'static str,
|
pub get_latest: &'static str,
|
||||||
|
pub search: Search,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Api {
|
impl Api {
|
||||||
const fn new() -> Api {
|
const fn new() -> Api {
|
||||||
let get_latest = "/api/v1/federated/latest";
|
let get_latest = "/api/v1/federated/latest";
|
||||||
Api { get_latest }
|
let search = Search::new();
|
||||||
|
Api { get_latest, search }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,3 +51,13 @@ impl Api {
|
||||||
pub struct LatestResp {
|
pub struct LatestResp {
|
||||||
pub latest: String,
|
pub latest: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct SearchRepositoryReq {
|
||||||
|
pub query: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct SearchRepositoryResp {
|
||||||
|
pub repositories: Vec<Repository>,
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
{
|
{
|
||||||
"db": "SQLite",
|
"db": "SQLite",
|
||||||
|
"069a127ffb1062321bba5a915a4ead3e0bbe4dabf6e0f684d8cc6a6d8a68ad5b": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "INSERT OR IGNORE INTO fts_repositories ( name, description, website, html_url ) \n VALUES ( $1, $2, $3, $4 );"
|
||||||
|
},
|
||||||
"0bb37cc79d5ef803285d05d06e6ef93b62c0b532c0298148fe436178761fd70a": {
|
"0bb37cc79d5ef803285d05d06e6ef93b62c0b532c0298148fe436178761fd70a": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
@ -18,6 +28,16 @@
|
||||||
},
|
},
|
||||||
"query": "SELECT ID FROM starchart_forges WHERE hostname = $1"
|
"query": "SELECT ID FROM starchart_forges WHERE hostname = $1"
|
||||||
},
|
},
|
||||||
|
"0f68a0cf8dbe0566dd6e0962c656382095694428dcf8990dc20d88601b56445b": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "INSERT OR IGNORE INTO fts_project_topics ( name ) VALUES ( $1 );"
|
||||||
|
},
|
||||||
"0fbcc736f60b14d55fbd88031a2929d04de02f5244345c2bc0f0e58d4c29cb14": {
|
"0fbcc736f60b14d55fbd88031a2929d04de02f5244345c2bc0f0e58d4c29cb14": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
@ -264,6 +284,16 @@
|
||||||
},
|
},
|
||||||
"query": "SELECT \n starchart_forges.hostname,\n starchart_users.username,\n starchart_repositories.name,\n starchart_repositories.description,\n starchart_repositories.html_url,\n starchart_repositories.ID,\n starchart_repositories.website,\n starchart_repositories.imported\n FROM\n starchart_repositories\n INNER JOIN\n starchart_forges\n ON\n starchart_repositories.hostname_id = starchart_forges.id\n INNER JOIN\n starchart_users\n ON\n starchart_repositories.owner_id = starchart_users.id\n ORDER BY\n starchart_repositories.ID\n LIMIT $1 OFFSET $2\n ;"
|
"query": "SELECT \n starchart_forges.hostname,\n starchart_users.username,\n starchart_repositories.name,\n starchart_repositories.description,\n starchart_repositories.html_url,\n starchart_repositories.ID,\n starchart_repositories.website,\n starchart_repositories.imported\n FROM\n starchart_repositories\n INNER JOIN\n starchart_forges\n ON\n starchart_repositories.hostname_id = starchart_forges.id\n INNER JOIN\n starchart_users\n ON\n starchart_repositories.owner_id = starchart_users.id\n ORDER BY\n starchart_repositories.ID\n LIMIT $1 OFFSET $2\n ;"
|
||||||
},
|
},
|
||||||
|
"989fdcfc0088ac19ffbe29bba5349d2dcade134fa2b450769ec617d91f88ffe1": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "INSERT OR IGNORE INTO fts_users ( username ) VALUES ( $1 );"
|
||||||
|
},
|
||||||
"9978a056397522cf1375900bb00c55bc17685dcc9cb22127b21a24b194a1e536": {
|
"9978a056397522cf1375900bb00c55bc17685dcc9cb22127b21a24b194a1e536": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -274,6 +304,24 @@
|
||||||
},
|
},
|
||||||
"query": "INSERT INTO\n starchart_forges (hostname, verified_on, forge_type, imported) \n VALUES ($1, $2, (SELECT ID FROM starchart_forge_type WHERE name = $3), $4)"
|
"query": "INSERT INTO\n starchart_forges (hostname, verified_on, forge_type, imported) \n VALUES ($1, $2, (SELECT ID FROM starchart_forge_type WHERE name = $3), $4)"
|
||||||
},
|
},
|
||||||
|
"a60479593819cc7dc7ec87a2d7a25a8960dc2efa1f59c5187d2084c79e26e3f8": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "SELECT name FROM starchart_project_topics ORDER BY ID LIMIT $1 OFFSET $2;"
|
||||||
|
},
|
||||||
"a81dd4b5df666e22fac211092e7b8425d838dd9023aa2b17659352f30831944d": {
|
"a81dd4b5df666e22fac211092e7b8425d838dd9023aa2b17659352f30831944d": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
@ -292,6 +340,66 @@
|
||||||
},
|
},
|
||||||
"query": "SELECT ID FROM starchart_users WHERE username = $1 AND \n hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $2)"
|
"query": "SELECT ID FROM starchart_users WHERE username = $1 AND \n hostname_id = (SELECT ID FROM starchart_forges WHERE hostname = $2)"
|
||||||
},
|
},
|
||||||
|
"ae9295f46da1753fe91a633b5738533084005ad32915c19635f896c454284b6b": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "hostname",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "html_url",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ID",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Int64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "imported",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "SELECT \n starchart_forges.hostname,\n starchart_users.username,\n starchart_repositories.name,\n starchart_repositories.description,\n starchart_repositories.html_url,\n starchart_repositories.ID,\n starchart_repositories.website,\n starchart_repositories.imported\n FROM\n starchart_repositories\n INNER JOIN\n starchart_forges\n ON\n starchart_repositories.hostname_id = starchart_forges.id\n INNER JOIN\n starchart_users\n ON\n starchart_repositories.owner_id = starchart_users.id\n WHERE starchart_repositories.html_url = $1\n ;"
|
||||||
|
},
|
||||||
"ca22e5f6e7065cf2d4ffdbfac0084f9871de8cd9073d470cbf7eac2de2a73c47": {
|
"ca22e5f6e7065cf2d4ffdbfac0084f9871de8cd9073d470cbf7eac2de2a73c47": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -302,6 +410,24 @@
|
||||||
},
|
},
|
||||||
"query": "INSERT INTO \n starchart_repositories (\n hostname_id, owner_id, name, description, html_url, website, created,\n last_crawl, imported\n )\n VALUES (\n (SELECT ID FROM starchart_forges WHERE hostname = $1),\n (SELECT ID FROM starchart_users WHERE username = $2),\n $3, $4, $5, $6, $7, $8, $9\n );"
|
"query": "INSERT INTO \n starchart_repositories (\n hostname_id, owner_id, name, description, html_url, website, created,\n last_crawl, imported\n )\n VALUES (\n (SELECT ID FROM starchart_forges WHERE hostname = $1),\n (SELECT ID FROM starchart_users WHERE username = $2),\n $3, $4, $5, $6, $7, $8, $9\n );"
|
||||||
},
|
},
|
||||||
|
"d22d18f0c2b49a570ac95e1af5ba398b3d1c7a1a5a6780d6dc646077e466714c": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "SELECT username FROM starchart_users ORDER BY ID LIMIT $1 OFFSET $2"
|
||||||
|
},
|
||||||
"e30ccfaa6aeda8cf30a2b3e9134abd0c0420441c5ed05189c3be605b1405c8e9": {
|
"e30ccfaa6aeda8cf30a2b3e9134abd0c0420441c5ed05189c3be605b1405c8e9": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -312,6 +438,24 @@
|
||||||
},
|
},
|
||||||
"query": "\n INSERT INTO starchart_repository_topic_mapping ( topic_id, repository_id )\n VALUES (\n (SELECT ID FROM starchart_project_topics WHERE name = $1),\n (SELECT ID FROM starchart_repositories WHERE html_url = $2)\n );"
|
"query": "\n INSERT INTO starchart_repository_topic_mapping ( topic_id, repository_id )\n VALUES (\n (SELECT ID FROM starchart_project_topics WHERE name = $1),\n (SELECT ID FROM starchart_repositories WHERE html_url = $2)\n );"
|
||||||
},
|
},
|
||||||
|
"e570081e9603b61c9df03d5cbcfcc4bb23b1f6ba077448ed1b743ef3e0cfbf39": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "html_url",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
true
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "SELECT html_url FROM fts_repositories WHERE html_url MATCH $1;"
|
||||||
|
},
|
||||||
"ed20e6fcac091172778119cdfc68c8e511224205a0bb7c74df192dcdfbfe63f2": {
|
"ed20e6fcac091172778119cdfc68c8e511224205a0bb7c74df192dcdfbfe63f2": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
|
3
sqlx-data.json
Normal file
3
sqlx-data.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"db": "SQLite"
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ use actix_web_codegen_const_routes::get;
|
||||||
pub use api_routes::*;
|
pub use api_routes::*;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
|
use crate::search;
|
||||||
use crate::WebFederate;
|
use crate::WebFederate;
|
||||||
|
|
||||||
#[get(path = "ROUTES.get_latest")]
|
#[get(path = "ROUTES.get_latest")]
|
||||||
|
@ -32,4 +33,5 @@ pub async fn lastest(federate: WebFederate) -> ServiceResult<impl Responder> {
|
||||||
|
|
||||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(lastest);
|
cfg.service(lastest);
|
||||||
|
search::services(cfg);
|
||||||
}
|
}
|
||||||
|
|
136
src/search.rs
Normal file
136
src/search.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* ForgeFlux StarChart - A federated software forge spider
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use crate::errors::*;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::{HttpResponse, Responder};
|
||||||
|
use actix_web_codegen_const_routes::post;
|
||||||
|
|
||||||
|
use crate::WebDB;
|
||||||
|
|
||||||
|
pub use crate::api::{SearchRepositoryReq, ROUTES};
|
||||||
|
|
||||||
|
#[post(path = "ROUTES.search.repository")]
|
||||||
|
pub async fn search_repository(
|
||||||
|
payload: web::Json<SearchRepositoryReq>,
|
||||||
|
db: WebDB,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
let query = if !payload.query.contains('*') {
|
||||||
|
payload.query
|
||||||
|
} else {
|
||||||
|
format!("*{}*", payload.query)
|
||||||
|
};
|
||||||
|
println!("{}", query);
|
||||||
|
let resp = db.search_repository(&query).await?;
|
||||||
|
println!("search_repository method: {:?}", resp);
|
||||||
|
Ok(HttpResponse::Ok().json(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(search_repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use db_core::prelude::*;
|
||||||
|
|
||||||
|
use crate::tests::*;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn search_works() {
|
||||||
|
const URL: &str = "https://search-works-test.example.com";
|
||||||
|
const HTML_PROFILE_URL: &str = "https://search-works-test.example.com/user1";
|
||||||
|
const USERNAME: &str = "user1";
|
||||||
|
|
||||||
|
const REPO_NAME: &str = "asdlkfjaldsfjaksdf";
|
||||||
|
const HTML_REPO_URL: &str =
|
||||||
|
"https://search-works-test.example.com/user1/asdlkfjaldsfjaksdf";
|
||||||
|
const TAGS: [&str; 3] = ["test", "starchart", "spider"];
|
||||||
|
|
||||||
|
let (db, ctx, federate, _tmpdir) = sqlx_sqlite::get_ctx().await;
|
||||||
|
let app = get_app!(ctx, db, federate).await;
|
||||||
|
|
||||||
|
let url = Url::parse(URL).unwrap();
|
||||||
|
|
||||||
|
let create_forge_msg = CreateForge {
|
||||||
|
url: url.clone(),
|
||||||
|
forge_type: ForgeImplementation::Gitea,
|
||||||
|
import: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_user_msg = AddUser {
|
||||||
|
url: url.clone(),
|
||||||
|
html_link: HTML_PROFILE_URL,
|
||||||
|
profile_photo: None,
|
||||||
|
username: USERNAME,
|
||||||
|
import: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_repo_msg = AddRepository {
|
||||||
|
html_link: HTML_REPO_URL,
|
||||||
|
name: REPO_NAME,
|
||||||
|
tags: Some(TAGS.into()),
|
||||||
|
owner: USERNAME,
|
||||||
|
website: None,
|
||||||
|
description: None,
|
||||||
|
url,
|
||||||
|
import: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = db.delete_forge_instance(&create_forge_msg.url).await;
|
||||||
|
db.create_forge_instance(&create_forge_msg).await.unwrap();
|
||||||
|
assert!(
|
||||||
|
db.forge_exists(&create_forge_msg.url).await.unwrap(),
|
||||||
|
"forge creation failed, forge existence check failure"
|
||||||
|
);
|
||||||
|
|
||||||
|
// add user
|
||||||
|
db.add_user(&add_user_msg).await.unwrap();
|
||||||
|
// add repository
|
||||||
|
db.create_repository(&add_repo_msg).await.unwrap();
|
||||||
|
// verify repo exists
|
||||||
|
assert!(db
|
||||||
|
.repository_exists(add_repo_msg.name, add_repo_msg.owner, &add_repo_msg.url)
|
||||||
|
.await
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
|
// test starts
|
||||||
|
|
||||||
|
let payload = SearchRepositoryReq {
|
||||||
|
query: REPO_NAME[0..REPO_NAME.len() - 4].to_string(),
|
||||||
|
};
|
||||||
|
let search_res_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.search.repository).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(search_res_resp.status(), StatusCode::OK);
|
||||||
|
let search_res: Vec<Repository> = test::read_body_json(search_res_resp).await;
|
||||||
|
println!("{:?}", search_res);
|
||||||
|
assert!(!search_res.is_empty());
|
||||||
|
assert_eq!(search_res.first().as_ref().unwrap().name, REPO_NAME);
|
||||||
|
|
||||||
|
// test ends
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue