ProjectAPIDao expansion (#92)

Signed-off-by: MiniDigger <admin@minidigger.me>
This commit is contained in:
realDragonium 2020-08-29 10:41:47 +02:00 committed by MiniDigger
parent 3f8e1b6ff7
commit 7da99df639
4 changed files with 161 additions and 28 deletions

View File

@ -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;

View File

@ -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<PaginatedProjectResult> listProjects(String q, List<Category> categories, List<String> tags, String owner, ProjectSortingStrategy sort, boolean relevance, Long inLimit, Long inOffset, ApiAuthInfo apiAuthInfo) {
public ResponseEntity<PaginatedProjectResult> listProjects(String q, List<Category> categories, List<String> 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<Project> 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 {
}
}
}

View File

@ -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, " +
" <if(requesterId)> " +
" 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, " +
" <endif>" +
" 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
" <if(pluginId)> AND (p.plugin_id = :pluginId) <endif> " +
" <if(owner)> AND (p.owner_name = :owner) <endif> " +
" <if(!seeHidden)> AND (p.visibility = 1 OR (:requesterId = ANY(p.project_members) AND p.visibility != 5)) <endif> " +
" <if(categories)> AND (p.category in (<categories>)) <endif> " +
" <if(query)> AND ( <queryStatement> ) <endif> " +
" <if(tags)> 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 (<tags>) ) <endif> " +
" ORDER BY <orderBy> " +
" LIMIT :limit" +
" OFFSET :offset")
@DefineNamedBindings
List<Project> listProjects(String pluginId, String owner, boolean seeHidden, Long requesterId, @Define String orderBy,
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) @EnumByOrdinal List<Category> categories,
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List<String> 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, " +//"<user_actions_taken>" +
" p.visibility, " +
" <if(requesterId)> " +
" 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, " +
" <endif> "+
" 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<Project> listProjects(String pluginId, List<Category> categories, List<Tag> 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
" <if(pluginId)> AND (p.plugin_id = :pluginId) <endif> " +
" <if(owner)> AND (p.owner_name = :owner) <endif> " +
" <if(!seeHidden)> AND (p.visibility = 1 OR (:requesterId = ANY(p.project_members) AND p.visibility != 5)) <endif> " +
" <if(categories)> AND (p.category in (<categories>)) <endif> " +
" <if(query)> AND ( <queryStatement> ) <endif> " +
" <if(tags)> 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 (<tags>) ) <endif> " +
" ) sq")
@DefineNamedBindings
long countProjects(String pluginId, String owner, @Define boolean seeHidden, Long requesterId,
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) @EnumByOrdinal List<Category> categories,
@BindList(onEmpty = BindList.EmptyHandling.NULL_VALUE) List<String> tags, //TODO: implement tags with mc_version('data')
String query, @Define String queryStatement);
}

View File

@ -194,19 +194,63 @@ public class ProjectService {
}
// TODO move to API daos
public List<Project> getProjects(String pluginId, List<Category> categories, List<Tag> 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<Project> getProjects(String pluginId, List<Category> categories, List<Tag> 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<Category> categories, List<Tag> parsedTags, String query, String owner, boolean seeHidden, Long requesterId) {
return 1; // TODO count projects query
public long countProjects(String pluginId, List<Category> categories, List<Tag> 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<String> getTagsNames(List<Tag> tags){
return tags == null ? null : tags.stream().filter(tag -> tag.getData() == null).map(Tag::getName).collect(Collectors.toList());
}
private List<String> getTagsNamesAndVersion(List<Tag> tags){
return tags == null ? null : tags.stream().filter(tag -> tag.getData() != null).map(tag -> " (" + tag.getName() + "," + tag.getData() + ") ").collect(Collectors.toList());
}
}