-
- Showing {{ itemsPerPage * (currentPage - 1) + 1 }}–{{
- itemsPerPage * (currentPage - 1) + Math.min(itemsPerPage, paginatedProjects.length)
- }}
- of {{ filteredProjects.length }}
- {{
- currentFilterType === DEFAULT_FILTER_TYPE ? 'projects' : currentFilterType.toLowerCase()
- }}
+
+
+
+ Showing {{ pageStart }}–{{ pageEnd }} of {{ totalProjects }}
+ {{
+ currentFilterType === DEFAULT_FILTER_TYPE ? 'projects' : currentFilterType.toLowerCase()
+ }}
+
+
+
+
+
-
+
@@ -124,6 +137,7 @@
diff --git a/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json b/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json
new file mode 100644
index 0000000000..6872b92825
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n select id\n from mods\n where id = any($1)\n and status not in ('rejected', 'draft', 'withheld', 'withdrawn')\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8Array"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "079846a1e6a6b080e0e7f2e3efbb53b6526332433faf66de8716bc5cd2b12afd"
+}
diff --git a/apps/labrinth/.sqlx/query-119a59fcf4bb2f19f89002c712a67c75d30056143c0bcabdbd74bb4c7b442082.json b/apps/labrinth/.sqlx/query-119a59fcf4bb2f19f89002c712a67c75d30056143c0bcabdbd74bb4c7b442082.json
deleted file mode 100644
index fcd5b34f17..0000000000
--- a/apps/labrinth/.sqlx/query-119a59fcf4bb2f19f89002c712a67c75d30056143c0bcabdbd74bb4c7b442082.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n SELECT\n id,\n external_dependencies_count as \"external_dependencies_count!\"\n FROM (\n SELECT DISTINCT ON (m.id)\n m.id,\n m.queued,\n (\n SELECT COUNT(*)\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE v.mod_id = m.id\n AND d.dependency_file_name IS NOT NULL\n ) external_dependencies_count\n FROM mods m\n\n /* -- Temporarily, don't exclude projects in tech rev q\n\n -- exclude projects in tech review queue\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = m.id AND didws.status = 'pending'\n */\n\n WHERE\n m.status = $1\n /* AND didws.status IS NULL */ -- Temporarily don't exclude\n\n GROUP BY m.id\n ) t\n WHERE\n ($4::boolean IS NULL OR (external_dependencies_count > 0) = $4)\n ORDER BY queued ASC\n OFFSET $3\n LIMIT $2\n ",
- "describe": {
- "columns": [
- {
- "ordinal": 0,
- "name": "id",
- "type_info": "Int8"
- },
- {
- "ordinal": 1,
- "name": "external_dependencies_count!",
- "type_info": "Int8"
- }
- ],
- "parameters": {
- "Left": [
- "Text",
- "Int8",
- "Int8",
- "Bool"
- ]
- },
- "nullable": [
- false,
- null
- ]
- },
- "hash": "119a59fcf4bb2f19f89002c712a67c75d30056143c0bcabdbd74bb4c7b442082"
-}
diff --git a/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json b/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json
new file mode 100644
index 0000000000..c55b8c6c7c
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d.json
@@ -0,0 +1,24 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text",
+ "Text",
+ "Bool"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d"
+}
diff --git a/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json b/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json
new file mode 100644
index 0000000000..38d43c6043
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9.json
@@ -0,0 +1,28 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $7::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n )\n SELECT id\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ORDER BY\n CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text",
+ "TextArray",
+ "Text",
+ "Text",
+ "Bool",
+ "Text",
+ "Bool"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "22fbd4bf1c2088903842b2c0070fd08431fc3f984263faed7aaeb311fa6ac2a9"
+}
diff --git a/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json b/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json
new file mode 100644
index 0000000000..450ec6ad79
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json
@@ -0,0 +1,135 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n WITH filtered_projects AS (\n SELECT\n m.id,\n m.queued,\n m.published\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $6::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published\n FROM filtered_projects\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $4\n LIMIT $3\n ),\n page_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n page_external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN page_ids page ON page.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n CASE\n WHEN m.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(ppt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count\n FROM page_ids page\n INNER JOIN mods m ON m.id = page.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id\n LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec
\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "total_count!",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "slug?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "name?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "summary?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "status?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "requested_status?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 8,
+ "name": "queued?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 9,
+ "name": "published?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 10,
+ "name": "updated?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 11,
+ "name": "organization_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 12,
+ "name": "organization_name?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 13,
+ "name": "organization_icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 14,
+ "name": "owner_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 15,
+ "name": "owner_name?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 16,
+ "name": "owner_icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 17,
+ "name": "project_types?: Vec",
+ "type_info": "TextArray"
+ },
+ {
+ "ordinal": 18,
+ "name": "external_dependencies_count?",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text",
+ "TextArray",
+ "Int8",
+ "Int8",
+ "Text",
+ "Bool"
+ ]
+ },
+ "nullable": [
+ null,
+ false,
+ true,
+ false,
+ false,
+ true,
+ false,
+ true,
+ true,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ false,
+ true,
+ null,
+ null
+ ]
+ },
+ "hash": "5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b"
+}
diff --git a/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json b/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json
new file mode 100644
index 0000000000..91c2728ad9
--- /dev/null
+++ b/apps/labrinth/.sqlx/query-ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41.json
@@ -0,0 +1,138 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n WITH moderation_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.description,\n m.queued,\n m.published,\n m.organization_id,\n m.team_id,\n m.components\n FROM mods m\n WHERE\n m.status = $1\n AND (\n $9::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = m.id\n AND didws.status = 'pending'\n )\n )\n ),\n external_dependencies AS (\n SELECT\n v.mod_id,\n COUNT(*) AS external_dependencies_count\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE d.dependency_file_name IS NOT NULL\n GROUP BY v.mod_id\n ),\n version_project_types AS (\n SELECT\n v.mod_id,\n ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types\n FROM versions v\n INNER JOIN moderation_projects mp ON mp.id = v.mod_id\n INNER JOIN loaders_versions lv ON v.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id\n INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n WHERE v.status = ANY($2)\n GROUP BY v.mod_id\n ),\n queue_projects AS (\n SELECT\n mp.id,\n mp.slug,\n mp.name,\n mp.summary,\n mp.description,\n mp.queued,\n mp.published,\n search_organization.name AS organization_name,\n search_owner.username AS owner_name,\n CASE\n WHEN mp.components ? 'minecraft_server'\n THEN ARRAY_APPEND(\n ARRAY_REMOVE(\n COALESCE(vpt.project_types::text[], ARRAY[]::text[]),\n 'modpack'\n ),\n 'minecraft_java_server'\n )\n ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])\n END AS project_types,\n COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count\n FROM moderation_projects mp\n LEFT JOIN organizations search_organization\n ON search_organization.id = mp.organization_id\n AND $3::text IS NOT NULL\n LEFT JOIN LATERAL (\n SELECT\n u.username\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = mp.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) search_owner ON $3::text IS NOT NULL\n AND mp.organization_id IS NULL\n LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id\n LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id\n ),\n filtered_projects AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM queue_projects\n WHERE\n (\n $3::text IS NULL\n OR name ILIKE '%' || $3 || '%'\n OR slug ILIKE '%' || $3 || '%'\n OR summary ILIKE '%' || $3 || '%'\n OR description ILIKE '%' || $3 || '%'\n OR owner_name ILIKE '%' || $3 || '%'\n OR organization_name ILIKE '%' || $3 || '%'\n OR EXISTS (\n SELECT 1\n FROM UNNEST(project_types) AS searched_project_type(project_type)\n WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'\n )\n )\n AND (\n $4::text IS NULL\n OR ($4 = 'none' AND CARDINALITY(project_types) = 0)\n OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])\n OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND\n project_types[1] = $4\n )\n )\n AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)\n ),\n total AS (\n SELECT COUNT(*) AS total_count FROM filtered_projects\n ),\n page_ids AS (\n SELECT\n id,\n queued,\n published,\n project_types,\n external_dependencies_count\n FROM filtered_projects\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n OFFSET $7\n LIMIT $6\n ),\n page_projects AS (\n SELECT\n m.id,\n m.slug,\n m.name,\n m.summary,\n m.icon_url,\n m.status,\n m.requested_status,\n m.queued,\n m.published,\n m.updated,\n m.organization_id,\n m.team_id,\n o.name AS organization_name,\n o.icon_url AS organization_icon_url,\n owner.user_id AS owner_id,\n owner.username AS owner_name,\n owner.avatar_url AS owner_icon_url,\n page_ids.project_types,\n page_ids.external_dependencies_count\n FROM page_ids\n INNER JOIN mods m ON m.id = page_ids.id\n LEFT JOIN organizations o ON o.id = m.organization_id\n LEFT JOIN LATERAL (\n SELECT\n tm.user_id,\n u.username,\n u.avatar_url\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = m.team_id\n AND tm.is_owner\n ORDER BY tm.ordering ASC\n LIMIT 1\n ) owner ON m.organization_id IS NULL\n )\n SELECT\n total.total_count AS \"total_count!\",\n page_projects.id AS \"id?\",\n page_projects.slug AS \"slug?\",\n page_projects.name AS \"name?\",\n page_projects.summary AS \"summary?\",\n page_projects.icon_url AS \"icon_url?\",\n page_projects.status AS \"status?\",\n page_projects.requested_status AS \"requested_status?\",\n page_projects.queued AS \"queued?\",\n page_projects.published AS \"published?\",\n page_projects.updated AS \"updated?\",\n page_projects.organization_id AS \"organization_id?\",\n page_projects.organization_name AS \"organization_name?\",\n page_projects.organization_icon_url AS \"organization_icon_url?\",\n page_projects.owner_id AS \"owner_id?\",\n page_projects.owner_name AS \"owner_name?\",\n page_projects.owner_icon_url AS \"owner_icon_url?\",\n page_projects.project_types AS \"project_types?: Vec\",\n page_projects.external_dependencies_count AS \"external_dependencies_count?\"\n FROM total\n LEFT JOIN page_projects ON true\n ORDER BY\n CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,\n CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,\n CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,\n CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,\n page_projects.id ASC\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "total_count!",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 1,
+ "name": "id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 2,
+ "name": "slug?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 3,
+ "name": "name?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "summary?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "status?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "requested_status?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 8,
+ "name": "queued?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 9,
+ "name": "published?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 10,
+ "name": "updated?",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 11,
+ "name": "organization_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 12,
+ "name": "organization_name?",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 13,
+ "name": "organization_icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 14,
+ "name": "owner_id?",
+ "type_info": "Int8"
+ },
+ {
+ "ordinal": 15,
+ "name": "owner_name?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 16,
+ "name": "owner_icon_url?",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 17,
+ "name": "project_types?: Vec",
+ "type_info": "TextArray"
+ },
+ {
+ "ordinal": 18,
+ "name": "external_dependencies_count?",
+ "type_info": "Int8"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Text",
+ "TextArray",
+ "Text",
+ "Text",
+ "Bool",
+ "Int8",
+ "Int8",
+ "Text",
+ "Bool"
+ ]
+ },
+ "nullable": [
+ null,
+ false,
+ true,
+ false,
+ false,
+ true,
+ false,
+ true,
+ true,
+ false,
+ false,
+ true,
+ false,
+ true,
+ false,
+ false,
+ true,
+ null,
+ null
+ ]
+ },
+ "hash": "ca8493b59004511e8d26200638951d55a61c2b6466db69a81be975e150b3ef41"
+}
diff --git a/apps/labrinth/src/routes/internal/mod.rs b/apps/labrinth/src/routes/internal/mod.rs
index ac0a212f29..e407df1972 100644
--- a/apps/labrinth/src/routes/internal/mod.rs
+++ b/apps/labrinth/src/routes/internal/mod.rs
@@ -101,6 +101,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
pats::edit_pat,
pats::delete_pat,
moderation::get_projects,
+ moderation::get_project_ids,
moderation::get_project_meta,
moderation::set_project_meta,
moderation::acquire_lock,
diff --git a/apps/labrinth/src/routes/internal/moderation/mod.rs b/apps/labrinth/src/routes/internal/moderation/mod.rs
index b230830cf5..7b3dac3ca1 100644
--- a/apps/labrinth/src/routes/internal/moderation/mod.rs
+++ b/apps/labrinth/src/routes/internal/moderation/mod.rs
@@ -5,8 +5,8 @@ use crate::database::PgPool;
use crate::database::models::DBModerationLock;
use crate::database::models::moderation_external_item;
use crate::database::redis::RedisPool;
-use crate::models::ids::OrganizationId;
-use crate::models::projects::{Project, ProjectStatus};
+use crate::models::ids::{OrganizationId, ProjectId};
+use crate::models::projects::{ProjectStatus, VersionStatus};
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
use crate::queue::session::AuthQueue;
use crate::util::error::Context;
@@ -17,7 +17,7 @@ use crate::{
use actix_web::{HttpRequest, delete, get, post, web};
use ariadne::ids::{UserId, random_base62};
use chrono::{DateTime, Utc};
-use ownership::get_projects_ownership;
+use eyre::eyre;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -27,6 +27,7 @@ pub mod tech_review;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_projects)
+ .service(get_project_ids)
.service(get_project_meta)
.service(set_project_meta)
.service(acquire_lock)
@@ -52,19 +53,74 @@ pub struct ProjectsRequestOptions {
/// Whether to filter by modpacks that have external dependencies.
#[serde(default)]
pub has_external_dependencies: Option,
+ /// Whether to exclude projects with pending technical review issues.
+ #[serde(default)]
+ pub exclude_technical_review: bool,
+ /// Text query to search against project and owner fields.
+ #[serde(default)]
+ pub query: Option,
+ /// Project type to filter by. Use `none` for projects without a type.
+ #[serde(default)]
+ pub project_type: Option,
+ /// Sort order for the moderation queue.
+ #[serde(default)]
+ pub sort: Option,
}
fn default_count() -> u16 {
100
}
-/// Project with extra information fetched from the database, to avoid having
-/// clients make more round trips.
+const MAX_PROJECTS_PER_PAGE: u16 = 200;
+
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, utoipa::ToSchema, Default,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum ModerationProjectsSort {
+ #[default]
+ Oldest,
+ Newest,
+ MostExternalDeps,
+ LeastExternalDeps,
+}
+
+impl ModerationProjectsSort {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Oldest => "oldest",
+ Self::Newest => "newest",
+ Self::MostExternalDeps => "most_external_deps",
+ Self::LeastExternalDeps => "least_external_deps",
+ }
+ }
+}
+
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
-pub struct FetchedProject {
- /// Project info.
- #[serde(flatten)]
- pub project: Project,
+pub struct ModerationProjectsResponse {
+ pub total: i64,
+ pub projects: Vec,
+}
+
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
+pub struct ModerationProjectIdsResponse {
+ pub ids: Vec,
+}
+
+/// Lightweight project information for the moderation queue index.
+#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
+pub struct ModerationQueueProject {
+ pub id: ProjectId,
+ pub slug: Option,
+ pub name: String,
+ pub summary: String,
+ pub icon_url: Option,
+ pub status: ProjectStatus,
+ pub requested_status: Option,
+ pub queued: Option>,
+ pub published: DateTime,
+ pub updated: DateTime,
+ pub project_types: Vec,
/// Who owns the project.
pub ownership: Ownership,
/// How many external file dependencies the project has.
@@ -162,12 +218,16 @@ pub struct DeleteAllLocksResponse {
#[utoipa::path(
context_path = "/moderation",
tag = "moderation",
- params(
+ params(
("count" = Option, Query),
("offset" = Option, Query),
- ("has_external_dependencies" = Option, Query)
+ ("has_external_dependencies" = Option, Query),
+ ("exclude_technical_review" = bool, Query),
+ ("query" = Option, Query),
+ ("project_type" = Option, Query),
+ ("sort" = Option, Query)
),
- responses((status = OK, body = inline(Vec)))
+ responses((status = OK, body = ModerationProjectsResponse))
)]
#[get("/projects")]
pub async fn get_projects(
@@ -176,7 +236,7 @@ pub async fn get_projects(
redis: web::Data,
request_opts: web::Query,
session_queue: web::Data,
-) -> Result>, ApiError> {
+) -> Result, ApiError> {
get_projects_internal(req, pool, redis, request_opts, session_queue).await
}
@@ -186,7 +246,7 @@ pub async fn get_projects_internal(
redis: web::Data,
request_opts: web::Query,
session_queue: web::Data,
-) -> Result>, ApiError> {
+) -> Result, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
@@ -196,102 +256,803 @@ pub async fn get_projects_internal(
)
.await?;
- use futures::stream::TryStreamExt;
+ let request_opts = request_opts.into_inner();
+ let query = normalize_optional_string(request_opts.query.as_deref());
+ let project_type =
+ normalize_optional_string(request_opts.project_type.as_deref());
+ let sort = request_opts.sort.unwrap_or_default().as_str();
+ let listed_version_statuses = listed_version_statuses();
+ let count = request_opts.count.min(MAX_PROJECTS_PER_PAGE) as i64;
+ let offset = request_opts.offset as i64;
+ let exclude_technical_review = request_opts.exclude_technical_review;
+ let needs_filtered_queue = query.is_some()
+ || project_type.is_some()
+ || request_opts.has_external_dependencies.is_some()
+ || matches!(sort, "most_external_deps" | "least_external_deps");
- let project_rows = sqlx::query!(
- r#"
- SELECT
- id,
- external_dependencies_count as "external_dependencies_count!"
- FROM (
- SELECT DISTINCT ON (m.id)
- m.id,
- m.queued,
- (
- SELECT COUNT(*)
- FROM versions v
- INNER JOIN dependencies d ON d.dependent_id = v.id
- WHERE v.mod_id = m.id
- AND d.dependency_file_name IS NOT NULL
- ) external_dependencies_count
- FROM mods m
-
- /* -- Temporarily, don't exclude projects in tech rev q
-
- -- exclude projects in tech review queue
- LEFT JOIN delphi_issue_details_with_statuses didws
- ON didws.project_id = m.id AND didws.status = 'pending'
- */
-
- WHERE
- m.status = $1
- /* AND didws.status IS NULL */ -- Temporarily don't exclude
-
- GROUP BY m.id
- ) t
- WHERE
- ($4::boolean IS NULL OR (external_dependencies_count > 0) = $4)
- ORDER BY queued ASC
- OFFSET $3
- LIMIT $2
- "#,
- ProjectStatus::Processing.as_str(),
- request_opts.count as i64,
- request_opts.offset as i64,
- request_opts.has_external_dependencies,
- )
- .fetch(&**pool)
- .try_collect::>()
- .await
- .wrap_internal_err("failed to fetch projects awaiting review")?;
-
- let project_ids = project_rows
- .iter()
- .map(|m| database::models::DBProjectId(m.id))
- .collect::>();
- let project_metadata = project_rows
- .into_iter()
- .map(|m| {
- (
- database::models::DBProjectId(m.id),
- m.external_dependencies_count,
+ let (total, projects) = if needs_filtered_queue {
+ let project_rows = sqlx::query!(
+ r#"
+ WITH moderation_projects AS (
+ SELECT
+ m.id,
+ m.slug,
+ m.name,
+ m.summary,
+ m.description,
+ m.queued,
+ m.published,
+ m.organization_id,
+ m.team_id,
+ m.components
+ FROM mods m
+ WHERE
+ m.status = $1
+ AND (
+ $9::boolean = false
+ OR NOT EXISTS (
+ SELECT 1
+ FROM delphi_issue_details_with_statuses didws
+ WHERE didws.project_id = m.id
+ AND didws.status = 'pending'
+ )
+ )
+ ),
+ external_dependencies AS (
+ SELECT
+ v.mod_id,
+ COUNT(*) AS external_dependencies_count
+ FROM versions v
+ INNER JOIN moderation_projects mp ON mp.id = v.mod_id
+ INNER JOIN dependencies d ON d.dependent_id = v.id
+ WHERE d.dependency_file_name IS NOT NULL
+ GROUP BY v.mod_id
+ ),
+ version_project_types AS (
+ SELECT
+ v.mod_id,
+ ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types
+ FROM versions v
+ INNER JOIN moderation_projects mp ON mp.id = v.mod_id
+ INNER JOIN loaders_versions lv ON v.id = lv.version_id
+ INNER JOIN loaders l ON lv.loader_id = l.id
+ INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id
+ INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
+ WHERE v.status = ANY($2)
+ GROUP BY v.mod_id
+ ),
+ queue_projects AS (
+ SELECT
+ mp.id,
+ mp.slug,
+ mp.name,
+ mp.summary,
+ mp.description,
+ mp.queued,
+ mp.published,
+ search_organization.name AS organization_name,
+ search_owner.username AS owner_name,
+ CASE
+ WHEN mp.components ? 'minecraft_server'
+ THEN ARRAY_APPEND(
+ ARRAY_REMOVE(
+ COALESCE(vpt.project_types::text[], ARRAY[]::text[]),
+ 'modpack'
+ ),
+ 'minecraft_java_server'
+ )
+ ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])
+ END AS project_types,
+ COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count
+ FROM moderation_projects mp
+ LEFT JOIN organizations search_organization
+ ON search_organization.id = mp.organization_id
+ AND $3::text IS NOT NULL
+ LEFT JOIN LATERAL (
+ SELECT
+ u.username
+ FROM team_members tm
+ INNER JOIN users u ON u.id = tm.user_id
+ WHERE tm.team_id = mp.team_id
+ AND tm.is_owner
+ ORDER BY tm.ordering ASC
+ LIMIT 1
+ ) search_owner ON $3::text IS NOT NULL
+ AND mp.organization_id IS NULL
+ LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id
+ LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id
+ ),
+ filtered_projects AS (
+ SELECT
+ id,
+ queued,
+ published,
+ project_types,
+ external_dependencies_count
+ FROM queue_projects
+ WHERE
+ (
+ $3::text IS NULL
+ OR name ILIKE '%' || $3 || '%'
+ OR slug ILIKE '%' || $3 || '%'
+ OR summary ILIKE '%' || $3 || '%'
+ OR description ILIKE '%' || $3 || '%'
+ OR owner_name ILIKE '%' || $3 || '%'
+ OR organization_name ILIKE '%' || $3 || '%'
+ OR EXISTS (
+ SELECT 1
+ FROM UNNEST(project_types) AS searched_project_type(project_type)
+ WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'
+ )
+ )
+ AND (
+ $4::text IS NULL
+ OR ($4 = 'none' AND CARDINALITY(project_types) = 0)
+ OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])
+ OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND
+ project_types[1] = $4
+ )
+ )
+ AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)
+ ),
+ total AS (
+ SELECT COUNT(*) AS total_count FROM filtered_projects
+ ),
+ page_ids AS (
+ SELECT
+ id,
+ queued,
+ published,
+ project_types,
+ external_dependencies_count
+ FROM filtered_projects
+ ORDER BY
+ CASE WHEN $8 = 'most_external_deps' THEN external_dependencies_count END DESC,
+ CASE WHEN $8 = 'least_external_deps' THEN external_dependencies_count END ASC,
+ CASE WHEN $8 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,
+ CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,
+ id ASC
+ OFFSET $7
+ LIMIT $6
+ ),
+ page_projects AS (
+ SELECT
+ m.id,
+ m.slug,
+ m.name,
+ m.summary,
+ m.icon_url,
+ m.status,
+ m.requested_status,
+ m.queued,
+ m.published,
+ m.updated,
+ m.organization_id,
+ m.team_id,
+ o.name AS organization_name,
+ o.icon_url AS organization_icon_url,
+ owner.user_id AS owner_id,
+ owner.username AS owner_name,
+ owner.avatar_url AS owner_icon_url,
+ page_ids.project_types,
+ page_ids.external_dependencies_count
+ FROM page_ids
+ INNER JOIN mods m ON m.id = page_ids.id
+ LEFT JOIN organizations o ON o.id = m.organization_id
+ LEFT JOIN LATERAL (
+ SELECT
+ tm.user_id,
+ u.username,
+ u.avatar_url
+ FROM team_members tm
+ INNER JOIN users u ON u.id = tm.user_id
+ WHERE tm.team_id = m.team_id
+ AND tm.is_owner
+ ORDER BY tm.ordering ASC
+ LIMIT 1
+ ) owner ON m.organization_id IS NULL
)
- })
- .collect::>();
-
- let projects =
- database::DBProject::get_many_ids(&project_ids, &**pool, &redis)
- .await
- .wrap_internal_err("failed to fetch projects")?
- .into_iter()
- .map(crate::models::projects::Project::from)
- .collect::>();
-
- let ownerships = get_projects_ownership(&projects, &pool, &redis)
+ SELECT
+ total.total_count AS "total_count!",
+ page_projects.id AS "id?",
+ page_projects.slug AS "slug?",
+ page_projects.name AS "name?",
+ page_projects.summary AS "summary?",
+ page_projects.icon_url AS "icon_url?",
+ page_projects.status AS "status?",
+ page_projects.requested_status AS "requested_status?",
+ page_projects.queued AS "queued?",
+ page_projects.published AS "published?",
+ page_projects.updated AS "updated?",
+ page_projects.organization_id AS "organization_id?",
+ page_projects.organization_name AS "organization_name?",
+ page_projects.organization_icon_url AS "organization_icon_url?",
+ page_projects.owner_id AS "owner_id?",
+ page_projects.owner_name AS "owner_name?",
+ page_projects.owner_icon_url AS "owner_icon_url?",
+ page_projects.project_types AS "project_types?: Vec",
+ page_projects.external_dependencies_count AS "external_dependencies_count?"
+ FROM total
+ LEFT JOIN page_projects ON true
+ ORDER BY
+ CASE WHEN $8 = 'most_external_deps' THEN page_projects.external_dependencies_count END DESC,
+ CASE WHEN $8 = 'least_external_deps' THEN page_projects.external_dependencies_count END ASC,
+ CASE WHEN $8 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,
+ CASE WHEN $8 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,
+ page_projects.id ASC
+ "#,
+ ProjectStatus::Processing.as_str(),
+ &listed_version_statuses,
+ query,
+ project_type,
+ request_opts.has_external_dependencies,
+ count,
+ offset,
+ sort,
+ exclude_technical_review,
+ )
+ .fetch_all(&**pool)
.await
- .wrap_internal_err("failed to fetch project ownerships")?;
+ .wrap_internal_err("failed to fetch filtered projects awaiting review")?;
- let map_project =
- |(project, ownership): (Project, Ownership)| -> FetchedProject {
- let external_dependencies_count = project_metadata
- .get(&database::models::DBProjectId(project.id.0 as i64))
- .copied()
- .unwrap_or_default();
-
- FetchedProject {
- ownership,
- project,
- external_dependencies_count,
+ let total =
+ project_rows.first().map(|row| row.total_count).unwrap_or(0);
+ let mut projects = Vec::new();
+ for row in project_rows {
+ if let Some(project) = row_to_queue_project(
+ row.id,
+ row.slug,
+ row.name,
+ row.summary,
+ row.icon_url,
+ row.status,
+ row.requested_status,
+ row.queued,
+ row.published,
+ row.updated,
+ row.organization_id,
+ row.organization_name,
+ row.organization_icon_url,
+ row.owner_id,
+ row.owner_name,
+ row.owner_icon_url,
+ row.project_types,
+ row.external_dependencies_count,
+ )? {
+ projects.push(project);
}
- };
+ }
- let projects = projects
- .into_iter()
- .zip(ownerships)
- .map(map_project)
- .collect::>();
+ (total, projects)
+ } else {
+ let project_rows = sqlx::query!(
+ r#"
+ WITH filtered_projects AS (
+ SELECT
+ m.id,
+ m.queued,
+ m.published
+ FROM mods m
+ WHERE
+ m.status = $1
+ AND (
+ $6::boolean = false
+ OR NOT EXISTS (
+ SELECT 1
+ FROM delphi_issue_details_with_statuses didws
+ WHERE didws.project_id = m.id
+ AND didws.status = 'pending'
+ )
+ )
+ ),
+ total AS (
+ SELECT COUNT(*) AS total_count FROM filtered_projects
+ ),
+ page_ids AS (
+ SELECT
+ id,
+ queued,
+ published
+ FROM filtered_projects
+ ORDER BY
+ CASE WHEN $5 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,
+ CASE WHEN $5 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,
+ id ASC
+ OFFSET $4
+ LIMIT $3
+ ),
+ page_project_types AS (
+ SELECT
+ v.mod_id,
+ ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types
+ FROM versions v
+ INNER JOIN page_ids page ON page.id = v.mod_id
+ INNER JOIN loaders_versions lv ON v.id = lv.version_id
+ INNER JOIN loaders l ON lv.loader_id = l.id
+ INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id
+ INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
+ WHERE v.status = ANY($2)
+ GROUP BY v.mod_id
+ ),
+ page_external_dependencies AS (
+ SELECT
+ v.mod_id,
+ COUNT(*) AS external_dependencies_count
+ FROM versions v
+ INNER JOIN page_ids page ON page.id = v.mod_id
+ INNER JOIN dependencies d ON d.dependent_id = v.id
+ WHERE d.dependency_file_name IS NOT NULL
+ GROUP BY v.mod_id
+ ),
+ page_projects AS (
+ SELECT
+ m.id,
+ m.slug,
+ m.name,
+ m.summary,
+ m.icon_url,
+ m.status,
+ m.requested_status,
+ m.queued,
+ m.published,
+ m.updated,
+ m.organization_id,
+ o.name AS organization_name,
+ o.icon_url AS organization_icon_url,
+ owner.user_id AS owner_id,
+ owner.username AS owner_name,
+ owner.avatar_url AS owner_icon_url,
+ CASE
+ WHEN m.components ? 'minecraft_server'
+ THEN ARRAY_APPEND(
+ ARRAY_REMOVE(
+ COALESCE(ppt.project_types::text[], ARRAY[]::text[]),
+ 'modpack'
+ ),
+ 'minecraft_java_server'
+ )
+ ELSE COALESCE(ppt.project_types::text[], ARRAY[]::text[])
+ END AS project_types,
+ COALESCE(ped.external_dependencies_count, 0) AS external_dependencies_count
+ FROM page_ids page
+ INNER JOIN mods m ON m.id = page.id
+ LEFT JOIN organizations o ON o.id = m.organization_id
+ LEFT JOIN LATERAL (
+ SELECT
+ tm.user_id,
+ u.username,
+ u.avatar_url
+ FROM team_members tm
+ INNER JOIN users u ON u.id = tm.user_id
+ WHERE tm.team_id = m.team_id
+ AND tm.is_owner
+ ORDER BY tm.ordering ASC
+ LIMIT 1
+ ) owner ON m.organization_id IS NULL
+ LEFT JOIN page_project_types ppt ON ppt.mod_id = m.id
+ LEFT JOIN page_external_dependencies ped ON ped.mod_id = m.id
+ )
+ SELECT
+ total.total_count AS "total_count!",
+ page_projects.id AS "id?",
+ page_projects.slug AS "slug?",
+ page_projects.name AS "name?",
+ page_projects.summary AS "summary?",
+ page_projects.icon_url AS "icon_url?",
+ page_projects.status AS "status?",
+ page_projects.requested_status AS "requested_status?",
+ page_projects.queued AS "queued?",
+ page_projects.published AS "published?",
+ page_projects.updated AS "updated?",
+ page_projects.organization_id AS "organization_id?",
+ page_projects.organization_name AS "organization_name?",
+ page_projects.organization_icon_url AS "organization_icon_url?",
+ page_projects.owner_id AS "owner_id?",
+ page_projects.owner_name AS "owner_name?",
+ page_projects.owner_icon_url AS "owner_icon_url?",
+ page_projects.project_types AS "project_types?: Vec",
+ page_projects.external_dependencies_count AS "external_dependencies_count?"
+ FROM total
+ LEFT JOIN page_projects ON true
+ ORDER BY
+ CASE WHEN $5 = 'newest' THEN COALESCE(page_projects.queued, page_projects.published) END DESC NULLS LAST,
+ CASE WHEN $5 = 'oldest' THEN COALESCE(page_projects.queued, page_projects.published) END ASC NULLS LAST,
+ page_projects.id ASC
+ "#,
+ ProjectStatus::Processing.as_str(),
+ &listed_version_statuses,
+ count,
+ offset,
+ sort,
+ exclude_technical_review,
+ )
+ .fetch_all(&**pool)
+ .await
+ .wrap_internal_err("failed to fetch projects awaiting review")?;
- Ok(web::Json(projects))
+ let total =
+ project_rows.first().map(|row| row.total_count).unwrap_or(0);
+ let mut projects = Vec::new();
+ for row in project_rows {
+ if let Some(project) = row_to_queue_project(
+ row.id,
+ row.slug,
+ row.name,
+ row.summary,
+ row.icon_url,
+ row.status,
+ row.requested_status,
+ row.queued,
+ row.published,
+ row.updated,
+ row.organization_id,
+ row.organization_name,
+ row.organization_icon_url,
+ row.owner_id,
+ row.owner_name,
+ row.owner_icon_url,
+ row.project_types,
+ row.external_dependencies_count,
+ )? {
+ projects.push(project);
+ }
+ }
+
+ (total, projects)
+ };
+
+ Ok(web::Json(ModerationProjectsResponse { total, projects }))
+}
+
+#[utoipa::path(
+ context_path = "/moderation",
+ tag = "moderation",
+ params(
+ ("has_external_dependencies" = Option, Query),
+ ("exclude_technical_review" = bool, Query),
+ ("query" = Option, Query),
+ ("project_type" = Option, Query),
+ ("sort" = Option, Query)
+ ),
+ responses((status = OK, body = ModerationProjectIdsResponse))
+)]
+#[get("/projects/ids")]
+pub async fn get_project_ids(
+ req: HttpRequest,
+ pool: web::Data,
+ redis: web::Data,
+ request_opts: web::Query,
+ session_queue: web::Data,
+) -> Result, ApiError> {
+ check_is_moderator_from_headers(
+ &req,
+ &**pool,
+ &redis,
+ &session_queue,
+ Scopes::PROJECT_READ,
+ )
+ .await?;
+
+ let request_opts = request_opts.into_inner();
+ let query = normalize_optional_string(request_opts.query.as_deref());
+ let project_type =
+ normalize_optional_string(request_opts.project_type.as_deref());
+ let sort = request_opts.sort.unwrap_or_default().as_str();
+ let listed_version_statuses = listed_version_statuses();
+ let exclude_technical_review = request_opts.exclude_technical_review;
+ let needs_filtered_queue = query.is_some()
+ || project_type.is_some()
+ || request_opts.has_external_dependencies.is_some()
+ || matches!(sort, "most_external_deps" | "least_external_deps");
+
+ let ids = if needs_filtered_queue {
+ let project_rows = sqlx::query!(
+ r#"
+ WITH moderation_projects AS (
+ SELECT
+ m.id,
+ m.slug,
+ m.name,
+ m.summary,
+ m.description,
+ m.queued,
+ m.published,
+ m.organization_id,
+ m.team_id,
+ m.components
+ FROM mods m
+ WHERE
+ m.status = $1
+ AND (
+ $7::boolean = false
+ OR NOT EXISTS (
+ SELECT 1
+ FROM delphi_issue_details_with_statuses didws
+ WHERE didws.project_id = m.id
+ AND didws.status = 'pending'
+ )
+ )
+ ),
+ external_dependencies AS (
+ SELECT
+ v.mod_id,
+ COUNT(*) AS external_dependencies_count
+ FROM versions v
+ INNER JOIN moderation_projects mp ON mp.id = v.mod_id
+ INNER JOIN dependencies d ON d.dependent_id = v.id
+ WHERE d.dependency_file_name IS NOT NULL
+ GROUP BY v.mod_id
+ ),
+ version_project_types AS (
+ SELECT
+ v.mod_id,
+ ARRAY_AGG(DISTINCT pt.name::text) FILTER (WHERE pt.name IS NOT NULL) AS project_types
+ FROM versions v
+ INNER JOIN moderation_projects mp ON mp.id = v.mod_id
+ INNER JOIN loaders_versions lv ON v.id = lv.version_id
+ INNER JOIN loaders l ON lv.loader_id = l.id
+ INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id
+ INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
+ WHERE v.status = ANY($2)
+ GROUP BY v.mod_id
+ ),
+ queue_projects AS (
+ SELECT
+ mp.id,
+ mp.slug,
+ mp.name,
+ mp.summary,
+ mp.description,
+ mp.queued,
+ mp.published,
+ search_organization.name AS organization_name,
+ search_owner.username AS owner_name,
+ CASE
+ WHEN mp.components ? 'minecraft_server'
+ THEN ARRAY_APPEND(
+ ARRAY_REMOVE(
+ COALESCE(vpt.project_types::text[], ARRAY[]::text[]),
+ 'modpack'
+ ),
+ 'minecraft_java_server'
+ )
+ ELSE COALESCE(vpt.project_types::text[], ARRAY[]::text[])
+ END AS project_types,
+ COALESCE(ed.external_dependencies_count, 0) AS external_dependencies_count
+ FROM moderation_projects mp
+ LEFT JOIN organizations search_organization
+ ON search_organization.id = mp.organization_id
+ AND $3::text IS NOT NULL
+ LEFT JOIN LATERAL (
+ SELECT
+ u.username
+ FROM team_members tm
+ INNER JOIN users u ON u.id = tm.user_id
+ WHERE tm.team_id = mp.team_id
+ AND tm.is_owner
+ ORDER BY tm.ordering ASC
+ LIMIT 1
+ ) search_owner ON $3::text IS NOT NULL
+ AND mp.organization_id IS NULL
+ LEFT JOIN external_dependencies ed ON ed.mod_id = mp.id
+ LEFT JOIN version_project_types vpt ON vpt.mod_id = mp.id
+ )
+ SELECT id
+ FROM queue_projects
+ WHERE
+ (
+ $3::text IS NULL
+ OR name ILIKE '%' || $3 || '%'
+ OR slug ILIKE '%' || $3 || '%'
+ OR summary ILIKE '%' || $3 || '%'
+ OR description ILIKE '%' || $3 || '%'
+ OR owner_name ILIKE '%' || $3 || '%'
+ OR organization_name ILIKE '%' || $3 || '%'
+ OR EXISTS (
+ SELECT 1
+ FROM UNNEST(project_types) AS searched_project_type(project_type)
+ WHERE searched_project_type.project_type ILIKE '%' || $3 || '%'
+ )
+ )
+ AND (
+ $4::text IS NULL
+ OR ($4 = 'none' AND CARDINALITY(project_types) = 0)
+ OR ($4 = 'minecraft_java_server' AND project_types @> ARRAY['minecraft_java_server']::text[])
+ OR ($4 <> 'none' AND $4 <> 'minecraft_java_server' AND project_types[1] = $4)
+ )
+ AND ($5::boolean IS NULL OR (external_dependencies_count > 0) = $5)
+ ORDER BY
+ CASE WHEN $6 = 'most_external_deps' THEN external_dependencies_count END DESC,
+ CASE WHEN $6 = 'least_external_deps' THEN external_dependencies_count END ASC,
+ CASE WHEN $6 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,
+ CASE WHEN $6 IN ('oldest', 'most_external_deps', 'least_external_deps') THEN COALESCE(queued, published) END ASC NULLS LAST,
+ id ASC
+ "#,
+ ProjectStatus::Processing.as_str(),
+ &listed_version_statuses,
+ query,
+ project_type,
+ request_opts.has_external_dependencies,
+ sort,
+ exclude_technical_review,
+ )
+ .fetch_all(&**pool)
+ .await
+ .wrap_internal_err("failed to fetch filtered project ids awaiting review")?;
+
+ project_rows
+ .into_iter()
+ .map(|row| ProjectId::from(database::models::DBProjectId(row.id)))
+ .collect()
+ } else {
+ let project_rows = sqlx::query!(
+ r#"
+ SELECT id
+ FROM mods
+ WHERE
+ status = $1
+ AND (
+ $3::boolean = false
+ OR NOT EXISTS (
+ SELECT 1
+ FROM delphi_issue_details_with_statuses didws
+ WHERE didws.project_id = mods.id
+ AND didws.status = 'pending'
+ )
+ )
+ ORDER BY
+ CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,
+ CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,
+ id ASC
+ "#,
+ ProjectStatus::Processing.as_str(),
+ sort,
+ exclude_technical_review,
+ )
+ .fetch_all(&**pool)
+ .await
+ .wrap_internal_err("failed to fetch project ids awaiting review")?;
+
+ project_rows
+ .into_iter()
+ .map(|row| ProjectId::from(database::models::DBProjectId(row.id)))
+ .collect()
+ };
+
+ Ok(web::Json(ModerationProjectIdsResponse { ids }))
+}
+
+fn row_to_queue_project(
+ id: Option,
+ slug: Option,
+ name: Option,
+ summary: Option,
+ icon_url: Option,
+ status: Option,
+ requested_status: Option,
+ queued: Option>,
+ published: Option>,
+ updated: Option>,
+ organization_id: Option,
+ organization_name: Option,
+ organization_icon_url: Option,
+ owner_id: Option,
+ owner_name: Option,
+ owner_icon_url: Option,
+ project_types: Option>,
+ external_dependencies_count: Option,
+) -> Result