From 7da99df63983604e561a9e58c4f73e3493ed3d15 Mon Sep 17 00:00:00 2001 From: realDragonium Date: Sat, 29 Aug 2020 10:41:47 +0200 Subject: [PATCH] ProjectAPIDao expansion (#92) Signed-off-by: MiniDigger --- docker/db/init.sql | 25 ++++++ .../controller/api/ProjectsApiController.java | 13 +-- .../hangar/db/dao/api/ProjectApiDao.java | 85 ++++++++++++++++--- .../service/project/ProjectService.java | 66 +++++++++++--- 4 files changed, 161 insertions(+), 28 deletions(-) diff --git a/docker/db/init.sql b/docker/db/init.sql index 6f85338f..3150e78b 100644 --- a/docker/db/init.sql +++ b/docker/db/init.sql @@ -1207,3 +1207,28 @@ END; $$; alter function logged_action_type_from_int(integer) owner to hangar; + +create function websearch_to_tsquery_postfix(dictionary regconfig, query text) returns tsquery + immutable + strict + language plpgsql +as $$ +DECLARE + arr TEXT[] := regexp_split_to_array(query, '\s+'); + last TEXT := websearch_to_tsquery('simple', arr[array_length(arr, 1)])::TEXT; + init TSQUERY := websearch_to_tsquery(dictionary, regexp_replace(query, '\S+$', '')); +BEGIN + IF last = '' THEN + BEGIN + RETURN init && $2::TSQUERY; + EXCEPTION + WHEN SYNTAX_ERROR THEN + RETURN init && websearch_to_tsquery(''); + END; + END IF; + + RETURN init && (websearch_to_tsquery(dictionary, last) || to_tsquery('simple', last || ':*')); +END; +$$; + +alter function websearch_to_tsquery_postfix(regconfig, text) owner to hangar; diff --git a/src/main/java/io/papermc/hangar/controller/api/ProjectsApiController.java b/src/main/java/io/papermc/hangar/controller/api/ProjectsApiController.java index 0bcd1b99..26f112e1 100644 --- a/src/main/java/io/papermc/hangar/controller/api/ProjectsApiController.java +++ b/src/main/java/io/papermc/hangar/controller/api/ProjectsApiController.java @@ -29,8 +29,10 @@ import org.springframework.stereotype.Controller; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Controller public class ProjectsApiController implements ProjectsApi { @@ -54,7 +56,7 @@ public class ProjectsApiController implements ProjectsApi { @Override @PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.util.ApiScope).forGlobal())") - public ResponseEntity listProjects(String q, List categories, List tags, String owner, ProjectSortingStrategy sort, boolean relevance, Long inLimit, Long inOffset, ApiAuthInfo apiAuthInfo) { + public ResponseEntity listProjects(String q, List categories, List tags, String owner, ProjectSortingStrategy sort, boolean orderWithRelevance, Long inLimit, Long inOffset, ApiAuthInfo apiAuthInfo) { // handle input long limit = ApiUtil.limitOrDefault(inLimit, hangarConfig.getProjects().getInitLoad()); long offset = ApiUtil.offsetOrZero(inOffset); @@ -74,8 +76,10 @@ public class ProjectsApiController implements ProjectsApi { boolean seeHidden = currentUser != null && permissionService.getGlobalPermissions(currentUser.getId()).has(Permission.SeeHidden); Long requesterId = currentUser == null ? null : currentUser.getId(); + String pluginId = null; + List projects = projectService.getProjects( - null, + pluginId, categories, parsedTags, q, @@ -83,13 +87,13 @@ public class ProjectsApiController implements ProjectsApi { seeHidden, requesterId, sort, - relevance, + orderWithRelevance, limit, offset ); long count = projectService.countProjects( - null, + pluginId, categories, parsedTags, q, @@ -140,5 +144,4 @@ public class ProjectsApiController implements ProjectsApi { } } - } diff --git a/src/main/java/io/papermc/hangar/db/dao/api/ProjectApiDao.java b/src/main/java/io/papermc/hangar/db/dao/api/ProjectApiDao.java index f08107fe..dbd30e5c 100644 --- a/src/main/java/io/papermc/hangar/db/dao/api/ProjectApiDao.java +++ b/src/main/java/io/papermc/hangar/db/dao/api/ProjectApiDao.java @@ -1,26 +1,72 @@ package io.papermc.hangar.db.dao.api; -import io.papermc.hangar.db.mappers.PromotedVersionMapper; -import io.papermc.hangar.model.Category; -import io.papermc.hangar.model.generated.Project; -import io.papermc.hangar.model.generated.Tag; - import org.jdbi.v3.core.enums.EnumByOrdinal; import org.jdbi.v3.sqlobject.config.RegisterBeanMapper; -import org.jdbi.v3.sqlobject.config.RegisterColumnMapper; -import org.jdbi.v3.sqlobject.customizer.AllowUnusedBindings; +import org.jdbi.v3.sqlobject.customizer.BindList; +import org.jdbi.v3.sqlobject.customizer.Define; +import org.jdbi.v3.sqlobject.customizer.DefineNamedBindings; import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.stringtemplate4.UseStringTemplateEngine; import org.springframework.stereotype.Repository; import java.util.List; +import io.papermc.hangar.model.Category; +import io.papermc.hangar.model.generated.Project; + @Repository @RegisterBeanMapper(Project.class) public interface ProjectApiDao { @UseStringTemplateEngine @SqlQuery("SELECT p.created_at," + + " p.plugin_id," + + " p.name," + + " p.owner_name," + + " p.slug," + + " p.promoted_versions," + + " p.views," + + " p.downloads," + + " p.recent_views," + + " p.recent_downloads," + + " p.stars," + + " p.watchers," + + " p.category," + + " p.description," + + " COALESCE(p.last_updated, p.created_at) AS last_updated," + + " p.visibility, " + + " " + + " EXISTS(SELECT * FROM project_stars s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS user_stared, " + + " EXISTS(SELECT * FROM project_watchers s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS user_watching, " + + " " + + " ps.homepage," + + " ps.issues," + + " ps.source," + + " ps.support," + + " ps.license_name," + + " ps.license_url," + + " ps.forum_sync" + + " FROM home_projects p" + + " JOIN projects ps ON p.id = ps.id" + + " WHERE true " + //Not sure how else to get here a single Where + " AND (p.plugin_id = :pluginId) " + + " AND (p.owner_name = :owner) " + + " AND (p.visibility = 1 OR (:requesterId = ANY(p.project_members) AND p.visibility != 5)) " + + " AND (p.category in ()) " + + " AND ( ) " + + " AND EXISTS ( SELECT pv.tag_name FROM jsonb_to_recordset(p.promoted_versions) " + + " AS pv(tag_name TEXT, tag_version TEXT) WHERE (pv.tag_name) in () ) " + + " ORDER BY " + + " LIMIT :limit" + + " OFFSET :offset") + @DefineNamedBindings + List listProjects(String pluginId, String owner, boolean seeHidden, Long requesterId, @Define String orderBy, + @BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) @EnumByOrdinal List categories, + @BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List tags, //TODO: implement tags with mc_version('data') + String query, @Define String queryStatement, long limit, long offset); + + @UseStringTemplateEngine + @SqlQuery("SELECT COUNT(*) FROM ( SELECT p.created_at," + " p.plugin_id," + " p.name," + " p.owner_name," + @@ -35,7 +81,11 @@ public interface ProjectApiDao { " p.category," + " p.description," + " COALESCE(p.last_updated, p.created_at) AS last_updated," + - " p.visibility, " +//"" + + " p.visibility, " + + " " + + " EXISTS(SELECT * FROM project_stars s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS user_stared, " + + " EXISTS(SELECT * FROM project_watchers s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS user_watching, " + + " "+ " ps.homepage," + " ps.issues," + " ps.source," + @@ -44,8 +94,19 @@ public interface ProjectApiDao { " ps.license_url," + " ps.forum_sync" + " FROM home_projects p" + - " JOIN projects ps ON p.id = ps.id") // TODO add all the missing filters - @RegisterColumnMapper(PromotedVersionMapper.class) - @AllowUnusedBindings // todo remove - List listProjects(String pluginId, List categories, List tags, String query, String owner, boolean seeHidden, Long requesterId, String ordering, long limit, long offset); + " JOIN projects ps ON p.id = ps.id" + + " WHERE true " + //Not sure how else to get here a single Where + " AND (p.plugin_id = :pluginId) " + + " AND (p.owner_name = :owner) " + + " AND (p.visibility = 1 OR (:requesterId = ANY(p.project_members) AND p.visibility != 5)) " + + " AND (p.category in ()) " + + " AND ( ) " + + " AND EXISTS ( SELECT pv.tag_name FROM jsonb_to_recordset(p.promoted_versions) " + + " AS pv(tag_name TEXT, tag_version TEXT) WHERE (pv.tag_name) in () ) " + + " ) sq") + @DefineNamedBindings + long countProjects(String pluginId, String owner, @Define boolean seeHidden, Long requesterId, + @BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) @EnumByOrdinal List categories, + @BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List tags, //TODO: implement tags with mc_version('data') + String query, @Define String queryStatement); } diff --git a/src/main/java/io/papermc/hangar/service/project/ProjectService.java b/src/main/java/io/papermc/hangar/service/project/ProjectService.java index 40862e29..e8358213 100644 --- a/src/main/java/io/papermc/hangar/service/project/ProjectService.java +++ b/src/main/java/io/papermc/hangar/service/project/ProjectService.java @@ -194,19 +194,63 @@ public class ProjectService { } // TODO move to API daos - public List getProjects(String pluginId, List categories, List tags, String query, String owner, boolean seeHidden, Long requesterId, ProjectSortingStrategy sort, boolean relevance, long limit, long offset) { - String ordering; - if (relevance && query != null && !query.isEmpty()) { - // TODO implement ordering by relevance, see APIVV2Queries#199 - ordering = sort.getSql(); - } else { - ordering = sort.getSql(); + public List getProjects(String pluginId, List categories, List tags, String query, String owner, boolean seeHidden, Long requesterId, ProjectSortingStrategy sort, boolean orderWithRelevance, long limit, long offset) { + String ordering = sort.getSql(); + if (orderWithRelevance && query != null && !query.isEmpty()) { + String relevance = "ts_rank(p.search_words, websearch_to_tsquery_postfix('english', :query)) DESC"; + if(query.endsWith(" ")) { + relevance = "ts_rank(p.search_words, websearch_to_tsquery('english', :query)) DESC"; + } + String orderingFirstHalf; + // 1483056000 is the Ore epoch + // 86400 seconds to days + // 604800‬ seconds to weeks + switch(sort){ + case STARS: orderingFirstHalf = "p.starts * "; break; + case DOWNLOADS: orderingFirstHalf ="(p.downloads / 100) * "; break; + case VIEWS: orderingFirstHalf ="(p.views / 200) *"; break; + case NEWEST: orderingFirstHalf ="((EXTRACT(EPOCH FROM p.created_at) - 1483056000) / 86400) *"; break; + case UPDATED: orderingFirstHalf ="((EXTRACT(EPOCH FROM p.last_updated) - 1483056000) / 604800) *"; break; + case ONLY_RELEVANCE: orderingFirstHalf =""; break; + case RECENT_DOWNLOADS : orderingFirstHalf ="p.recent_views *"; break; + case RECENT_VIEWS: orderingFirstHalf ="p.recent_downloads*"; break; + default: + orderingFirstHalf = " "; //Just in case and so that the ide doesnt complain + } + ordering = orderingFirstHalf + relevance; } - - return projectApiDao.get().listProjects(pluginId, categories, tags, query, owner, seeHidden, requesterId, ordering, limit, offset); + return projectApiDao.get().listProjects(pluginId, owner, seeHidden, requesterId, ordering, categories, getTagsNames(tags), trimQuery(query), getQueryStatement(query), limit, offset); } - public long countProjects(String pluginId, List categories, List parsedTags, String query, String owner, boolean seeHidden, Long requesterId) { - return 1; // TODO count projects query + public long countProjects(String pluginId, List categories, List tags, String query, String owner, boolean seeHidden, Long requesterId) { + return projectApiDao.get().countProjects(pluginId, owner, seeHidden, requesterId, categories, getTagsNames(tags), trimQuery(query), getQueryStatement(query)); + } + + private String trimQuery(String query){ + String trimmedQuery = null; + if(query != null && !query.isBlank()) { + trimmedQuery = query.trim(); //Ore#APIV2Queries line 169 && 200 + } + return trimmedQuery; + } + + private String getQueryStatement(String query){ + String queryStatement = null; + if(query != null && !query.isBlank()){ + if(query.endsWith(" ")) { + queryStatement = "p.search_words @@ websearch_to_tsquery('english', :query)"; + } else { + queryStatement = "p.search_words @@ websearch_to_tsquery_postfix('english', :query)"; + } + } + return queryStatement; + } + + private List getTagsNames(List tags){ + return tags == null ? null : tags.stream().filter(tag -> tag.getData() == null).map(Tag::getName).collect(Collectors.toList()); + } + + private List getTagsNamesAndVersion(List tags){ + return tags == null ? null : tags.stream().filter(tag -> tag.getData() != null).map(tag -> " (" + tag.getName() + "," + tag.getData() + ") ").collect(Collectors.toList()); } }