homepage search/sort/filter

This commit is contained in:
MiniDigger | Martin 2022-06-18 01:52:28 +02:00
parent debd074388
commit 877d939e0a
16 changed files with 227 additions and 135 deletions

View File

@ -122,7 +122,9 @@
"mostDownloads": "Most Downloads",
"mostViews": "Most Views",
"newest": "Newest",
"recentlyUpdated": "Recently Updated"
"recentlyUpdated": "Recently Updated",
"recentViews": "Recent Views",
"recentDownloads": "Recent Downloads"
},
"category": {
"info": "Category",

View File

@ -32,6 +32,8 @@ const sorters = [
{ id: "views", label: i18n.t("project.sorting.mostViews") },
{ id: "newest", label: i18n.t("project.sorting.newest") },
{ id: "updated", label: i18n.t("project.sorting.recentlyUpdated") },
{ id: "recent_views", label: i18n.t("project.sorting.recentViews") },
{ id: "recent_downloads", label: i18n.t("project.sorting.recentDownloads") },
];
const versions = backendData.platforms
@ -45,20 +47,29 @@ const filters = ref({
versions: [],
categories: [],
platforms: [],
licences: [],
licenses: [],
});
const query = ref<string>((route.query.q as string) || "");
const loggedOut = ref<boolean>("loggedOut" in route.query);
const projects = ref<PaginatedResult<Project> | null>();
const activeSorter = ref<string>("updated");
const requestParams = computed(() => {
// TODO add filters for licence and version
// TODO implement sorting, see https://github.com/HangarMC/Hangar/pull/597
const params: Record<string, any> = { limit: 25, offset: 0, category: filters.value.categories, platform: filters.value.platforms };
if (query.value && query.value) {
// TODO add filter for mc version
const params: Record<string, any> = {
limit: 25,
offset: 0,
category: filters.value.categories,
platform: filters.value.platforms,
license: filters.value.licenses,
};
if (query.value) {
params.q = query.value;
}
if (activeSorter.value) {
params.sort = activeSorter.value;
}
return params;
});
const p = await useProjects(requestParams.value).catch((e) => handleRequestError(e, ctx, i18n));
@ -67,9 +78,8 @@ if (p && p.value) {
}
watch(filters, async () => updateProjects(), { deep: true });
watch(query, async () => {
await updateProjects();
});
watch(query, async () => updateProjects());
watch(activeSorter, async () => updateProjects());
async function updateProjects() {
projects.value = await useApi<PaginatedResult<Project>>("projects", false, "get", requestParams.value);
@ -127,7 +137,7 @@ useHead(meta);
>
<MenuItems class="absolute right-0 top-16 flex flex-col z-10 background-default drop-shadow-md rounded-md border-top-primary">
<MenuItem v-for="sorter in sorters" :key="sorter.id" v-slot="{ active }">
<button :class="{ 'bg-gradient-to-r from-[#004ee9] to-[#367aff] text-white': active }" class="p-2 text-left">
<button :class="{ 'bg-gradient-to-r from-[#004ee9] to-[#367aff] text-white': active }" class="p-2 text-left" @click="activeSorter = sorter.id">
{{ sorter.label }}
</button>
</MenuItem>
@ -175,7 +185,7 @@ useHead(meta);
<div class="licenses">
<h3 class="font-bold mb-1">Licenses</h3>
<div class="flex flex-col gap-1">
<InputCheckbox v-for="license in backendData.licenses" :key="license" v-model="filters.licences" :value="license" :label="license" />
<InputCheckbox v-for="license in backendData.licenses" :key="license" v-model="filters.licenses" :value="license" :label="license" />
</div>
</div>
</Card>

View File

@ -2,7 +2,9 @@ package io.papermc.hangar.controller.api.v1;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.controller.api.v1.interfaces.IProjectsController;
import io.papermc.hangar.controller.extras.pagination.SorterRegistry;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableFilters;
import io.papermc.hangar.controller.extras.pagination.annotations.ApplicableSorters;
import io.papermc.hangar.controller.extras.pagination.filters.projects.*;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.User;
@ -52,9 +54,10 @@ public class ProjectsController extends HangarComponent implements IProjectsCont
}
@Override
@ApplicableFilters({ProjectCategoryFilter.class, ProjectPlatformFilter.class, ProjectAuthorFilter.class, ProjectQueryFilter.class, ProjectTagFilter.class})
public ResponseEntity<PaginatedResult<Project>> getProjects(String q, ProjectSortingStrategy sort, boolean orderWithRelevance, @NotNull RequestPagination pagination) {
return ResponseEntity.ok(projectsApiService.getProjects(q, sort, orderWithRelevance, pagination));
@ApplicableFilters({ProjectCategoryFilter.class, ProjectPlatformFilter.class, ProjectAuthorFilter.class, ProjectQueryFilter.class, ProjectTagFilter.class, ProjectLicenseFilter.class})
@ApplicableSorters({SorterRegistry.VIEWS, SorterRegistry.DOWNLOADS, SorterRegistry.NEWEST, SorterRegistry.STARS, SorterRegistry.UPDATED, SorterRegistry.RECENT_DOWNLOADS, SorterRegistry.RECENT_VIEWS})
public ResponseEntity<PaginatedResult<Project>> getProjects(String q, boolean orderWithRelevance, @NotNull RequestPagination pagination) {
return ResponseEntity.ok(projectsApiService.getProjects(q, orderWithRelevance, pagination));
}
@Override

View File

@ -79,7 +79,6 @@ public interface IProjectsController {
@GetMapping("/projects")
ResponseEntity<PaginatedResult<Project>> getProjects(
@ApiParam("The query to use when searching") @RequestParam(required = false) String q,
@ApiParam("How to sort the projects") @RequestParam(defaultValue = "updated") ProjectSortingStrategy sort,
@ApiParam("If how relevant the project is to the given query should be used when sorting the projects") @RequestParam(defaultValue = "true") boolean relevance,
@ApiParam("Pagination information") @NotNull RequestPagination pagination
);

View File

@ -9,7 +9,16 @@ public enum SorterRegistry implements Sorter {
USER_JOIN_DATE("joinDate", simpleSorter("u.join_date")),
USER_NAME("username", simpleSorter("username")),
USER_PROJECT_COUNT("projectCount", simpleSorter("project_count"));
USER_PROJECT_COUNT("projectCount", simpleSorter("project_count")),
// For Projects
VIEWS("views", simpleSorter("hp.views")),
STARS("stars", simpleSorter("hp.stars")),
DOWNLOADS("downloads", simpleSorter("hp.downloads")),
NEWEST("newest", simpleSorter("hp.created_at")),
UPDATED("updated", simpleSorter("last_updated_double")),
RECENT_VIEWS("recent_views", simpleSorter("hp.recent_views")),
RECENT_DOWNLOADS("recent_downloads", simpleSorter("hp.recent_downloads"));
private static final Map<String, SorterRegistry> SORTERS = new HashMap<>();
@ -47,7 +56,7 @@ public enum SorterRegistry implements Sorter {
throw new IllegalArgumentException(name + " is not a registered sorter");
}
enum SortDirection {
public enum SortDirection {
ASCENDING(" ASC"),
DESCENDING(" DESC");

View File

@ -0,0 +1,69 @@
package io.papermc.hangar.controller.extras.pagination.filters.projects;
import org.jdbi.v3.core.statement.SqlStatement;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.NativeWebRequest;
import java.util.Arrays;
import java.util.Set;
import io.papermc.hangar.controller.extras.pagination.Filter;
@Component
public class ProjectLicenseFilter implements Filter<ProjectLicenseFilter.ProjectLicenseFilterInstance> {
private final ConversionService conversionService;
@Autowired
public ProjectLicenseFilter(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Set<String> getQueryParamNames() {
return Set.of("license");
}
@Override
public String getDescription() {
return "A license to filter for";
}
@NotNull
@Override
public ProjectLicenseFilterInstance create(NativeWebRequest webRequest) {
return new ProjectLicenseFilterInstance(conversionService.convert(webRequest.getParameterValues(getSingleQueryParam()), String[].class));
}
static class ProjectLicenseFilterInstance implements FilterInstance {
private final String[] licenses;
public ProjectLicenseFilterInstance(String[] licenses) {
this.licenses = licenses;
}
@Override
public void createSql(StringBuilder sb, SqlStatement<?> q) {
sb.append(" AND p.license_type").append(" IN (");
for (int i = 0; i < licenses.length; i++) {
sb.append(":__licenses__").append(i);
if (i + 1 != licenses.length) {
sb.append(",");
}
q.bind("__licenses__" + i, licenses[i]);
}
sb.append(")");
}
@Override
public String toString() {
return "ProjectLicenseFilterInstance{" +
"licenses=" + Arrays.toString(licenses) +
'}';
}
}
}

View File

@ -48,7 +48,7 @@ public class ProjectPlatformFilter implements Filter<ProjectPlatformFilter.Proje
@Override
public void createSql(StringBuilder sb, SqlStatement<?> q) {
sb.append("AND v.platform").append(" IN (");
sb.append(" AND v.platform").append(" IN (");
for (int i = 0; i < platforms.length; i++) {
sb.append(":__platform__").append(i);
if (i + 1 != platforms.length) {

View File

@ -38,14 +38,12 @@ public class ProjectQueryFilter implements Filter<ProjectQueryFilterInstance> {
@Override
public void createSql(StringBuilder sb, SqlStatement<?> q) {
/*sb.append(" AND (hp.search_words @@ websearch_to_tsquery");
sb.append(" AND (hp.search_words @@ websearch_to_tsquery");
if (!query.endsWith(" ")) {
sb.append("_postfix");
}
sb.append("('english', :query)").append(")");
q.bind("query", query.trim());*/
// TODO broken. Full-text search is not implemented in cockroachdb yet. See: https://go.crdb.dev/issue-v/7821/v21.2. Until it's done, we'll just do simple search
sb.append(" AND (hp.name ILIKE '%" + query + "%')");
q.bind("query", query.trim());
}
@Override

View File

@ -49,6 +49,7 @@ public interface HangarProjectsDAO {
" ps.source," +
" ps.support," +
" ps.license_name," +
" ps.license_type," +
" ps.license_url," +
" ps.keywords," +
" ps.forum_sync," +

View File

@ -21,13 +21,13 @@ public interface ProjectsDAO {
@Timestamped
@GetGeneratedKeys
@SqlUpdate("insert into projects (created_at, name, slug, owner_name, owner_id, category, description, visibility, homepage, issues, source, support, keywords, license_name, license_url, donation_enabled, donation_subject, sponsors) " +
"values (:now, :name, :slug, :ownerName,:ownerId, :category, :description, :visibility, :homepage, :issues, :source, :support, :keywords, :licenseName, :licenseUrl, :donationEnabled, :donationSubject, :sponsors)")
@SqlUpdate("insert into projects (created_at, name, slug, owner_name, owner_id, category, description, visibility, homepage, issues, source, support, keywords, license_type, license_name, license_url, donation_enabled, donation_subject, sponsors) " +
"values (:now, :name, :slug, :ownerName,:ownerId, :category, :description, :visibility, :homepage, :issues, :source, :support, :keywords, :licenseType, :licenseName, :licenseUrl, :donationEnabled, :donationSubject, :sponsors)")
ProjectTable insert(@BindBean ProjectTable project);
@GetGeneratedKeys
@SqlUpdate("UPDATE projects SET name = :name, slug = :slug, category = :category, keywords = :keywords, issues = :issues, source = :source, " +
"license_name = :licenseName, license_url = :licenseUrl, forum_sync = :forumSync, description = :description, visibility = :visibility, " +
"license_type = :licenseType, license_name = :licenseName, license_url = :licenseUrl, forum_sync = :forumSync, description = :description, visibility = :visibility, " +
"support = :support, homepage = :homepage, post_id = :postId, topic_id = :topicId, donation_enabled = :donationEnabled, donation_subject = :donationSubject, " +
"sponsors = :sponsors WHERE id = :id")
ProjectTable update(@BindBean ProjectTable project);

View File

@ -86,6 +86,7 @@ public interface ProjectsApiDAO {
COALESCE(hp.last_updated, hp.created_at) AS last_updated,
((EXTRACT(EPOCH FROM COALESCE(hp.last_updated, hp.created_at)) - 1609459200) / 604800) *1 AS last_updated_double, --- We can order with this. That "dum" does not work. It only orders it with this.
hp.visibility,
<relevance>
exists(SELECT * FROM project_stars s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS starred,
exists(SELECT * FROM project_watchers s WHERE s.project_id = p.id AND s.user_id = :requesterId) AS watching,
exists(SELECT * FROM project_flags pf WHERE pf.project_id = p.id AND pf.user_id = :requesterId AND pf.resolved IS FALSE) as flagged,
@ -94,6 +95,7 @@ public interface ProjectsApiDAO {
p.source,
p.support,
p.license_name,
p.license_type,
p.license_url,
p.keywords,
p.forum_sync,
@ -109,12 +111,12 @@ public interface ProjectsApiDAO {
LEFT JOIN platform_versions v on pvpd.platform_version_id = v.id
WHERE true <filters> -- Not sure how else to get here a single Where
<if(!seeHidden)> AND (hp.visibility = 0 <if(requesterId)>OR (:requesterId = ANY(hp.project_members) AND hp.visibility != 4)<endif>) <endif>
ORDER BY <orderBy>
<sorters>
<offsetLimit>""")
@RegisterColumnMapper(PromotedVersionMapper.class)
@DefineNamedBindings
List<Project> getProjects(@Define boolean seeHidden, Long requesterId, @Define String orderBy,
@BindPagination RequestPagination pagination);
List<Project> getProjects(@Define boolean seeHidden, Long requesterId,
@BindPagination RequestPagination pagination, @Define String relevance);
// This query can be shorter because it doesnt need all those column values as above does, just a single column for the amount of rows to be counted
@UseStringTemplateEngine

View File

@ -1,7 +1,6 @@
package io.papermc.hangar.model.api.requests;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.papermc.hangar.controller.extras.ApiUtils;
import io.papermc.hangar.controller.extras.pagination.Filter.FilterInstance;
import io.swagger.annotations.ApiModelProperty;

View File

@ -31,6 +31,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
private String issues;
private String source;
private String support;
private String licenseType;
private String licenseName;
private String licenseUrl;
private boolean forumSync;
@ -54,6 +55,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
this.source = form.getSettings().getSource();
this.support = form.getSettings().getSupport();
this.keywords = form.getSettings().getKeywords();
this.licenseType = form.getSettings().getLicense().getType();
this.licenseName = form.getSettings().getLicense().getName();
this.licenseUrl = form.getSettings().getLicense().getUrl();
this.donationEnabled = form.getSettings().getDonation().isEnable();
@ -76,6 +78,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
this.issues = other.issues;
this.source = other.source;
this.support = other.support;
this.licenseType = other.licenseType;
this.licenseName = other.licenseName;
this.licenseUrl = other.licenseUrl;
this.forumSync = other.forumSync;
@ -87,7 +90,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
@JdbiConstructor
public ProjectTable(OffsetDateTime createdAt, long id, String name, String slug, String ownerName, long ownerId, Long topicId,
Long postId, @EnumByOrdinal Category category, String description, @EnumByOrdinal Visibility visibility, Collection<String> keywords,
String homepage, String issues, String source, String support, String licenseName, String licenseUrl, boolean forumSync,
String homepage, String issues, String source, String support, String licenseType, String licenseName, String licenseUrl, boolean forumSync,
boolean donationEnabled, String donationSubject, String sponsors) {
super(createdAt, id);
this.name = name;
@ -104,6 +107,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
this.issues = issues;
this.source = source;
this.support = support;
this.licenseType = licenseType;
this.licenseName = licenseName;
this.licenseUrl = licenseUrl;
this.forumSync = forumSync;
@ -231,6 +235,14 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
this.support = support;
}
public String getLicenseType() {
return licenseType;
}
public void setLicenseType(String licenseType) {
this.licenseType = licenseType;
}
public String getLicenseName() {
return licenseName;
}
@ -306,6 +318,7 @@ public class ProjectTable extends Table implements Visitable, ModelVisible, Proj
", issues='" + issues + '\'' +
", source='" + source + '\'' +
", support='" + support + '\'' +
", licenseType='" + licenseType + '\'' +
", licenseName='" + licenseName + '\'' +
", licenseUrl='" + licenseUrl + '\'' +
", forumSync=" + forumSync +

View File

@ -1,6 +1,7 @@
package io.papermc.hangar.service.api;
import io.papermc.hangar.HangarComponent;
import io.papermc.hangar.controller.extras.pagination.SorterRegistry;
import io.papermc.hangar.db.dao.v1.ProjectsApiDAO;
import io.papermc.hangar.model.api.PaginatedResult;
import io.papermc.hangar.model.api.Pagination;
@ -16,8 +17,11 @@ import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class ProjectsApiService extends HangarComponent {
@ -53,37 +57,20 @@ public class ProjectsApiService extends HangarComponent {
return new PaginatedResult<>(new Pagination(projectsApiDAO.getProjectWatchersCount(author, slug), pagination), watchers);
}
public PaginatedResult<Project> getProjects(String query, ProjectSortingStrategy sort, boolean orderWithRelevance, RequestPagination pagination) {
String ordering = sort.getSql();
public PaginatedResult<Project> getProjects(String query, boolean orderWithRelevance, RequestPagination pagination) {
String relevance = "";
if (orderWithRelevance && query != null && !query.isEmpty()) {
String relevance = "ts_rank(hp.search_words, websearch_to_tsquery_postfix('english', :query)) DESC";
if(query.endsWith(" ")) {
relevance = "ts_rank(hp.search_words, websearch_to_tsquery('english', :query)) DESC";
relevance = "ts_rank(hp.search_words, websearch_to_tsquery('english', :query)) AS relevance,";
} else {
relevance = "ts_rank(hp.search_words, websearch_to_tsquery_postfix('english', :query)) AS relevance,";
}
relevance = "ASC";
String orderingFirstHalf;
// 1609459200 is the hangar epoch
// 86400 seconds to days
// 604800 seconds to weeks
switch(sort){
case STARS: orderingFirstHalf = "hp.stars * "; break;
case DOWNLOADS: orderingFirstHalf ="(hp.downloads / 100) * "; break;
case VIEWS: orderingFirstHalf ="(hp.views / 200) *"; break;
case NEWEST: orderingFirstHalf ="((EXTRACT(EPOCH FROM hp.created_at) - 1609459200) / 86400) *"; break;
case UPDATED: orderingFirstHalf ="last_updated_double "; break;
case ONLY_RELEVANCE: orderingFirstHalf = ""; break;
case RECENT_DOWNLOADS : orderingFirstHalf ="hp.recent_views *"; break;
case RECENT_VIEWS: orderingFirstHalf ="hp.recent_downloads *"; break;
default:
orderingFirstHalf = " "; // Just in case and so that the ide doesn't complain
}
ordering = orderingFirstHalf + relevance;
pagination.getSorters().put("relevance", sb -> sb.append(" relevance DESC"));
}
boolean seeHidden = getGlobalPermissions().has(Permission.SeeHidden);
List<Project> projects = projectsApiDAO.getProjects(seeHidden, getHangarUserId(), ordering, pagination);
List<Project> projects = projectsApiDAO.getProjects(seeHidden, getHangarUserId(), pagination, relevance);
return new PaginatedResult<>(new Pagination(projectsApiDAO.countProjects(seeHidden, getHangarUserId(), pagination), pagination), projects);
}
}

View File

@ -131,6 +131,7 @@ public class ProjectService extends HangarComponent {
if (licenseName == null) {
licenseName = settingsForm.getSettings().getLicense().getType();
}
projectTable.setLicenseName(settingsForm.getSettings().getLicense().getType());
projectTable.setLicenseName(licenseName);
projectTable.setLicenseUrl(settingsForm.getSettings().getLicense().getUrl());
projectTable.setForumSync(settingsForm.getSettings().isForumSync());

View File

@ -95,6 +95,7 @@ CREATE TABLE projects
issues varchar(255),
source varchar(255),
support varchar(255),
license_type varchar(255),
license_name varchar(255),
license_url varchar(255),
forum_sync boolean DEFAULT TRUE NOT NULL,
@ -960,6 +961,7 @@ SELECT p.id,
p.description,
p.name,
p.created_at,
p.license_type,
max(lv.created_at) AS last_updated,
to_jsonb(ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', tags.tag_name,
'tag_version', tags.tag_version, 'tag_color',
@ -967,21 +969,18 @@ SELECT p.id,
FROM tags
WHERE tags.project_id = p.id
LIMIT 5)) AS promoted_versions,
--TODO fix homepage view--
-- ((setweight((to_tsvector('english'::regconfig, p.name::text) ||
-- to_tsvector('english'::regconfig, regexp_replace(p.name::text, '([a-z])([A-Z]+)'::text,
-- '\1_\2'::text, 'g'::text))), 'A'::"char") ||
-- setweight(to_tsvector('english'::regconfig, p.description::text), 'B'::"char")) ||
-- setweight(to_tsvector('english'::regconfig, array_to_string(p.keywords, ' '::text)), 'C'::"char")) || setweight(
-- to_tsvector('english'::regconfig, p.owner_name::text) || to_tsvector('english'::regconfig,
-- regexp_replace(
-- p.owner_name::text,
-- '([a-z])([A-Z]+)'::text,
-- '\1_\2'::text,
-- 'g'::text)),
-- 'D'::"char") AS search_words
-- TODO fix homepage view
'DUM' AS search_words
((setweight((to_tsvector('english'::regconfig, p.name::text) ||
to_tsvector('english'::regconfig, regexp_replace(p.name::text, '([a-z])([A-Z]+)'::text,
'\1_\2'::text, 'g'::text))), 'A'::"char") ||
setweight(to_tsvector('english'::regconfig, p.description::text), 'B'::"char")) ||
setweight(to_tsvector('english'::regconfig, array_to_string(p.keywords, ' '::text)), 'C'::"char")) || setweight(
to_tsvector('english'::regconfig, p.owner_name::text) || to_tsvector('english'::regconfig,
regexp_replace(
p.owner_name::text,
'([a-z])([A-Z]+)'::text,
'\1_\2'::text,
'g'::text)),
'D'::"char") AS search_words
FROM projects p
LEFT JOIN project_versions lv ON p.id = lv.project_id
JOIN project_members_all pm ON p.id = pm.id
@ -1174,69 +1173,69 @@ FROM logged_actions_organization a
LEFT JOIN users u ON a.user_id = u.id
LEFT JOIN users s ON o.user_id = s.id;
-- CREATE FUNCTION delete_old_project_version_download_warnings() RETURNS TRIGGER
-- LANGUAGE plpgsql
-- AS $$
-- BEGIN
-- DELETE FROM project_version_download_warnings WHERE created_at < current_date - interval '30' day;
-- RETURN NEW;
-- END
-- $$;
--
-- CREATE TRIGGER clean_old_project_version_download_warnings
-- AFTER INSERT
-- ON project_version_download_warnings
-- EXECUTE PROCEDURE delete_old_project_version_download_warnings();
--
-- CREATE FUNCTION delete_old_project_version_unsafe_downloads() RETURNS TRIGGER
-- LANGUAGE plpgsql
-- AS $$
-- BEGIN
-- DELETE FROM project_version_unsafe_downloads WHERE created_at < current_date - interval '30' day;
-- RETURN NEW;
-- END
-- $$;
--
-- CREATE TRIGGER clean_old_project_version_unsafe_downloads
-- AFTER INSERT
-- ON project_version_unsafe_downloads
-- EXECUTE PROCEDURE delete_old_project_version_unsafe_downloads();
--
-- CREATE FUNCTION update_project_name_trigger() RETURNS TRIGGER
-- LANGUAGE plpgsql
-- AS $$
-- BEGIN
-- UPDATE projects p SET name = u.name FROM users u WHERE p.id = new.id AND u.id = new.owner_id;
-- END;
-- $$;
--
-- CREATE TRIGGER project_owner_name_updater
-- AFTER UPDATE
-- OF owner_id
-- ON projects
-- FOR EACH ROW
-- WHEN (old.owner_id <> new.owner_id)
-- EXECUTE PROCEDURE update_project_name_trigger();
--
-- 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;
-- $$;
CREATE FUNCTION delete_old_project_version_download_warnings() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM project_version_download_warnings WHERE created_at < current_date - interval '30' day;
RETURN NEW;
END
$$;
CREATE TRIGGER clean_old_project_version_download_warnings
AFTER INSERT
ON project_version_download_warnings
EXECUTE PROCEDURE delete_old_project_version_download_warnings();
CREATE FUNCTION delete_old_project_version_unsafe_downloads() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM project_version_unsafe_downloads WHERE created_at < current_date - interval '30' day;
RETURN NEW;
END
$$;
CREATE TRIGGER clean_old_project_version_unsafe_downloads
AFTER INSERT
ON project_version_unsafe_downloads
EXECUTE PROCEDURE delete_old_project_version_unsafe_downloads();
CREATE FUNCTION update_project_name_trigger() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE projects p SET name = u.name FROM users u WHERE p.id = new.id AND u.id = new.owner_id;
END;
$$;
CREATE TRIGGER project_owner_name_updater
AFTER UPDATE
OF owner_id
ON projects
FOR EACH ROW
WHEN (old.owner_id <> new.owner_id)
EXECUTE PROCEDURE update_project_name_trigger();
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;
$$;