From 157962e42aa8c244836ac1ea41914dab7afbb697 Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Wed, 2 Jun 2021 18:33:11 -0700 Subject: [PATCH] Improve peformance of search indexing, v2 fixes + new routes (#205) * Refactor search to not spam the database with queries, new utility routes for V2 * Run prepare --- sqlx-data.json | 786 +++++++++++++--------------- src/database/models/team_item.rs | 74 +++ src/models/teams.rs | 6 +- src/models/users.rs | 4 +- src/routes/mod.rs | 1 + src/routes/tags.rs | 9 +- src/routes/teams.rs | 89 +++- src/routes/users.rs | 27 +- src/routes/v1/mod.rs | 7 +- src/routes/v1/tags.rs | 31 +- src/routes/v1/teams.rs | 76 +++ src/routes/v1/users.rs | 40 +- src/search/indexing/local_import.rs | 419 +++++---------- src/search/indexing/mod.rs | 1 + src/search/mod.rs | 8 +- src/validate/fabric.rs | 6 +- src/validate/forge.rs | 8 +- src/validate/pack.rs | 4 +- 18 files changed, 833 insertions(+), 763 deletions(-) create mode 100644 src/routes/v1/teams.rs diff --git a/sqlx-data.json b/sqlx-data.json index 35f2ced32..07e830fe9 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -187,32 +187,6 @@ "nullable": [] } }, - "07e72d55c2f18744bbfffee9920866d4aacd680f316058ec734735c173a7f16b": { - "query": "\n SELECT DISTINCT gv.version, gv.created FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "created", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "07ebc9dc82cd012cd4f5880b1eb3d82602c195a3e3ddd557103ee037aa6dad1c": { "query": "\n INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n ", "describe": { @@ -261,6 +235,147 @@ ] } }, + "0bea37e9f2c4c962633fd3359702974962d9f264593bc603c72da5bc7ddff4c4": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published,\n m.updated updated,\n m.team_id team_id, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv ON gvv.game_version_id = gv.id\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders lo ON lo.id = lv.loader_id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $2\n INNER JOIN users u ON tm.user_id = u.id\n WHERE s.status = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 18, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 19, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 20, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + null, + null, + null + ] + } + }, "0ca11a32b2860e4f5c3d20892a5be3cb419e084f42ba0f98e09b9995027fcc4e": { "query": "\n SELECT id FROM statuses\n WHERE status = $1\n ", "describe": { @@ -281,33 +396,6 @@ ] } }, - "0da158263c6588a83421154342db2ede16b9abf9931827790b9fcaf71080c324": { - "query": "\n SELECT u.id, u.username FROM users u\n INNER JOIN team_members tm ON tm.user_id = u.id\n WHERE tm.team_id = $2 AND tm.role = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "0dbd0fa9a25416716a047184944d243ed5cb55808c6f300d7335c887f02a7f6e": { "query": "\n INSERT INTO report_types (name)\n VALUES ($1)\n ON CONFLICT (name) DO NOTHING\n RETURNING id\n ", "describe": { @@ -1021,108 +1109,6 @@ ] } }, - "255ebd215adf4e2a5a837b8a682b7396d185c656e1151fcec24eddca1b7fb942": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 10, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 11, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 13, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 14, - "name": "server_side", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false - ] - } - }, "29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": { "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n ", "describe": { @@ -1178,26 +1164,6 @@ "nullable": [] } }, - "31853f131eaeb2aaedc2bcc27da387462408409af810337f6a8ef397f674fb44": { - "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "loader", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", "describe": { @@ -1539,26 +1505,6 @@ "nullable": [] } }, - "3fdece422b1c54bc0853fa3ddbb4c1d1a45ac7723c906544ea9b4f69e5b29dc1": { - "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, "413762398111e04074a2d8a1e4e03ed362b9167d397947f8d14e5ae330e3de0b": { "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", "describe": { @@ -1571,26 +1517,6 @@ "nullable": [] } }, - "43660c74ef6a72b3fc68006a2f743737f1e4973788a5c954ffeaac151c16d0c1": { - "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "status", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", "describe": { @@ -1926,26 +1852,6 @@ ] } }, - "4c99c0840159d18e88cd6094a41117258f2337346c145d926b5b610c76b5125f": { - "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "category", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ", "describe": { @@ -2122,6 +2028,147 @@ ] } }, + "57d2a9f3dd9377fa7435deed1e09c474e447d7e502004556e239b6c2984e259b": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published,\n m.updated updated,\n m.team_id team_id, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv ON gvv.game_version_id = gv.id\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders lo ON lo.id = lv.loader_id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $2\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 10, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 11, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 18, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 19, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 20, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + null, + null, + null + ] + } + }, "5a13a79ebb1ab975f88b58e6deaba9685fe16e242c0fa4a5eea54f12f9448e6b": { "query": "\n DELETE FROM reports\n WHERE version_id = $1\n ", "describe": { @@ -2646,26 +2693,6 @@ ] } }, - "72c1e6de8f2c8d89be030454eeab6d5c9695164af2ebfb8d7e94b2deee4f130d": { - "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "category", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "72c75313688dfd88a659c5250c71b9899abd6186ab32a067a7d4b8a0846ebd18": { "query": "\n INSERT INTO game_versions (version, type, created)\n VALUES ($1, COALESCE($2, 'other'), COALESCE($3, timezone('utc', now())))\n ON CONFLICT (version) DO UPDATE\n SET type = COALESCE($2, game_versions.type),\n created = COALESCE($3, game_versions.created)\n RETURNING id\n ", "describe": { @@ -3413,26 +3440,6 @@ ] } }, - "9985165594f04fb1d68e2e415a996b6553e8b5c91f121df3a9194806df10a197": { - "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -3446,33 +3453,6 @@ "nullable": [] } }, - "9d95d136d0e6eedee57e6aa524232c02609b89e4e26032e07403aabb69bea0d8": { - "query": "\n SELECT u.id, u.username FROM users u\n INNER JOIN team_members tm ON tm.user_id = u.id\n WHERE tm.team_id = $2 AND tm.role = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "username", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text", - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "a39ce28b656032f862b205cffa393a76b989f4803654a615477a94fda5f57354": { "query": "\n DELETE FROM states\n WHERE id = $1\n ", "describe": { @@ -3731,32 +3711,6 @@ "nullable": [] } }, - "ad273daadd249e93b500c339a62aac48a497ebcc15164776ad20860a4d232896": { - "query": "\n SELECT DISTINCT gv.version, gv.created FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ORDER BY gv.created ASC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "created", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": { "query": "\n DELETE FROM teams\n WHERE id = $1\n ", "describe": { @@ -4752,13 +4706,73 @@ ] } }, - "d44c747044d9b1f6424e6a0eece324fd3e0964efdcb9ec7d18e70a2f67787914": { - "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", + "d2bba2670ef992df166a5e1e4d90f14f1d6b19c5fe77eb7139a5e1a0e660f6db": { + "query": "\n SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted,\n u.id user_id, u.github_id github_id, u.name user_name, u.email email,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = $1\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "loader", + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "member_role", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "permissions", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "accepted", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 5, + "name": "github_id", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "user_name", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "email", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "avatar_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "username", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "bio", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 12, + "name": "user_role", "type_info": "Varchar" } ], @@ -4768,6 +4782,18 @@ ] }, "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + false, + true, + false, false ] } @@ -5464,104 +5490,6 @@ ] } }, - "fa5fe155fafd3b10c12fe5cf9cbbe4a4b3eb59f0f2e6584753a72517bb2d4574": { - "query": "\n SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side\n FROM mods m\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 10, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 12, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 13, - "name": "server_side", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - true, - false, - false, - false, - true, - false, - false, - false - ] - } - }, "fa911efc808e726c13659d3ce6baf61dc562e6f1e73fd65537a4ab1dad17120e": { "query": "\n DELETE FROM downloads\n WHERE downloads.version_id = $1\n ", "describe": { diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index 2f69b1a73..c6be4dd2c 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -1,4 +1,5 @@ use super::ids::*; +use crate::database::models::User; use crate::models::teams::Permissions; pub struct TeamBuilder { @@ -78,6 +79,17 @@ pub struct TeamMember { pub accepted: bool, } +/// A member of a team +pub struct QueryTeamMember { + pub id: TeamMemberId, + pub team_id: TeamId, + /// The user associated with the member + pub user: User, + pub role: String, + pub permissions: Permissions, + pub accepted: bool, +} + impl TeamMember { /// Lists the members of a team pub async fn get_from_team<'a, 'b, E>( @@ -127,6 +139,68 @@ impl TeamMember { Ok(team_members) } + // Lists the full members of a team + pub async fn get_from_team_full<'a, 'b, E>( + id: TeamId, + executor: E, + ) -> Result, super::DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use futures::stream::TryStreamExt; + + let team_members = sqlx::query!( + " + SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, + u.id user_id, u.github_id github_id, u.name user_name, u.email email, + u.avatar_url avatar_url, u.username username, u.bio bio, + u.created created, u.role user_role + FROM team_members tm + INNER JOIN users u ON u.id = tm.user_id + WHERE tm.team_id = $1 + ", + id as TeamId, + ) + .fetch_many(executor) + .try_filter_map(|e| async { + if let Some(m) = e.right() { + let permissions = Permissions::from_bits(m.permissions as u64); + if let Some(perms) = permissions { + Ok(Some(Ok(QueryTeamMember { + id: TeamMemberId(m.id), + team_id: id, + role: m.member_role, + permissions: perms, + accepted: m.accepted, + user: User { + id: UserId(m.id), + github_id: m.github_id, + name: m.user_name, + email: m.email, + avatar_url: m.avatar_url, + username: m.username, + bio: m.bio, + created: m.created, + role: m.user_role, + }, + }))) + } else { + Ok(Some(Err(super::DatabaseError::BitflagError))) + } + } else { + Ok(None) + } + }) + .try_collect::>>() + .await?; + + let team_members = team_members + .into_iter() + .collect::, super::DatabaseError>>()?; + + Ok(team_members) + } + /// Lists the team members for a user. Does not list pending requests. pub async fn get_from_user_public<'a, 'b, E>( id: UserId, diff --git a/src/models/teams.rs b/src/models/teams.rs index 056013d94..1bed9b9e7 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -1,5 +1,5 @@ use super::ids::Base62Id; -use crate::models::users::UserId; +use crate::models::users::User; use serde::{Deserialize, Serialize}; /// The ID of a team @@ -47,8 +47,8 @@ impl Default for Permissions { pub struct TeamMember { /// The ID of the team this team member is a member of pub team_id: TeamId, - /// The ID of the user associated with the member - pub user_id: UserId, + /// The user associated with the member + pub user: User, /// The role of the user in the team pub role: String, /// A bitset containing the user's permissions in this team diff --git a/src/models/users.rs b/src/models/users.rs index c94d8e796..c0614aca4 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -8,7 +8,7 @@ pub struct UserId(pub u64); pub const DELETED_USER: UserId = UserId(127155982985829); -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct User { pub id: UserId, pub github_id: Option, @@ -21,7 +21,7 @@ pub struct User { pub role: Role, } -#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] pub enum Role { Developer, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 68a82ae49..0becc6ed3 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -54,6 +54,7 @@ pub fn projects_config(cfg: &mut web::ServiceConfig) { .service(projects::project_icon_edit) .service(projects::project_follow) .service(projects::project_unfollow) + .service(teams::team_members_get_project) .service(web::scope("{project_id}").service(versions::version_list)), ); } diff --git a/src/routes/tags.rs b/src/routes/tags.rs index edeff7127..ddb1b3cfa 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -41,7 +41,7 @@ pub struct CategoryData { // searching category list #[get("category")] pub async fn category_list(pool: web::Data) -> Result { - let results = Category::list(&**pool) + let mut results = Category::list(&**pool) .await? .into_iter() .map(|x| CategoryData { @@ -51,6 +51,8 @@ pub async fn category_list(pool: web::Data) -> Result>(); + results.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + Ok(HttpResponse::Ok().json(results)) } @@ -115,7 +117,7 @@ pub struct LoaderData { #[get("loader")] pub async fn loader_list(pool: web::Data) -> Result { - let results = Loader::list(&**pool) + let mut results = Loader::list(&**pool) .await? .into_iter() .map(|x| LoaderData { @@ -124,6 +126,9 @@ pub async fn loader_list(pool: web::Data) -> Result>(); + + results.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + Ok(HttpResponse::Ok().json(results)) } diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 93b4eb86d..651411a54 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -1,5 +1,6 @@ use crate::auth::get_user_from_headers; use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder}; +use crate::database::models::team_item::QueryTeamMember; use crate::database::models::TeamMember; use crate::models::ids::ProjectId; use crate::models::teams::{Permissions, TeamId}; @@ -9,6 +10,66 @@ use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +#[get("{id}/members")] +pub async fn team_members_get_project( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let string = info.into_inner().0; + let project_data = + crate::database::models::Project::get_from_slug_or_project_id(string, &**pool).await?; + + if let Some(project) = project_data { + let members_data = TeamMember::get_from_team_full(project.team_id, &**pool).await?; + + let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); + + if let Some(user) = current_user { + let team_member = + TeamMember::get_from_user_id(project.team_id, user.id.into(), &**pool) + .await + .map_err(ApiError::DatabaseError)?; + + if team_member.is_some() { + let team_members: Vec = members_data + .into_iter() + .map(|data| convert_team_member(data, false)) + .collect(); + + return Ok(HttpResponse::Ok().json(team_members)); + } + } + + let team_members: Vec = members_data + .into_iter() + .filter(|x| x.accepted) + .map(|data| convert_team_member(data, true)) + .collect(); + + Ok(HttpResponse::Ok().json(team_members)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +pub fn convert_team_member( + data: QueryTeamMember, + override_permissions: bool, +) -> crate::models::teams::TeamMember { + crate::models::teams::TeamMember { + team_id: data.team_id.into(), + user: super::users::convert_user(data.user), + role: data.role, + permissions: if override_permissions { + None + } else { + Some(data.permissions) + }, + accepted: data.accepted, + } +} + #[get("{id}/members")] pub async fn team_members_get( req: HttpRequest, @@ -16,7 +77,7 @@ pub async fn team_members_get( pool: web::Data, ) -> Result { let id = info.into_inner().0; - let members_data = TeamMember::get_from_team(id.into(), &**pool).await?; + let members_data = TeamMember::get_from_team_full(id.into(), &**pool).await?; let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); @@ -28,32 +89,18 @@ pub async fn team_members_get( if team_member.is_some() { let team_members: Vec = members_data .into_iter() - .map(|data| crate::models::teams::TeamMember { - team_id: id, - user_id: data.user_id.into(), - role: data.role, - permissions: Some(data.permissions), - accepted: data.accepted, - }) + .map(|data| convert_team_member(data, false)) .collect(); return Ok(HttpResponse::Ok().json(team_members)); } } - let mut team_members: Vec = Vec::new(); - - for team_member in members_data { - if team_member.accepted { - team_members.push(crate::models::teams::TeamMember { - team_id: id, - user_id: team_member.user_id.into(), - role: team_member.role, - permissions: None, - accepted: team_member.accepted, - }) - } - } + let team_members: Vec = members_data + .into_iter() + .filter(|x| x.accepted) + .map(|data| convert_team_member(data, true)) + .collect(); Ok(HttpResponse::Ok().json(team_members)) } diff --git a/src/routes/users.rs b/src/routes/users.rs index a51336c8a..98d15b54d 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,9 +1,8 @@ use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::file_hosting::FileHost; -use crate::models::ids::ProjectId; use crate::models::notifications::Notification; -use crate::models::projects::ProjectStatus; +use crate::models::projects::{Project, ProjectStatus}; use crate::models::users::{Role, UserId}; use crate::routes::notifications::convert_notification; use crate::routes::ApiError; @@ -75,7 +74,7 @@ pub async fn user_get( } } -fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User { +pub fn convert_user(data: crate::database::models::user_item::User) -> crate::models::users::User { crate::models::users::User { id: data.id.into(), github_id: data.github_id.map(|i| i as u64), @@ -114,10 +113,11 @@ pub async fn projects_list( User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? }; - let response = project_data + let response = crate::database::Project::get_many_full(project_data, &**pool) + .await? .into_iter() - .map(|v| v.into()) - .collect::>(); + .map(super::projects::convert_project) + .collect::>(); Ok(HttpResponse::Ok().json(response)) } else { @@ -433,7 +433,7 @@ pub async fn user_follows( use futures::TryStreamExt; - let projects: Vec = sqlx::query!( + let project_ids = sqlx::query!( " SELECT mf.mod_id FROM mod_follows mf WHERE mf.follower_id = $1 @@ -441,10 +441,19 @@ pub async fn user_follows( id as crate::database::models::ids::UserId, ) .fetch_many(&**pool) - .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) }) - .try_collect::>() + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ProjectId(m.mod_id))) + }) + .try_collect::>() .await?; + let projects = crate::database::Project::get_many_full(project_ids, &**pool) + .await? + .into_iter() + .map(super::projects::convert_project) + .collect::>(); + Ok(HttpResponse::Ok().json(projects)) } else { Ok(HttpResponse::NotFound().body("")) diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs index 03e5af0ff..38611607d 100644 --- a/src/routes/v1/mod.rs +++ b/src/routes/v1/mod.rs @@ -4,6 +4,7 @@ mod moderation; mod mods; mod reports; mod tags; +mod teams; mod users; mod versions; @@ -31,7 +32,7 @@ pub fn tags_config(cfg: &mut web::ServiceConfig) { .service(tags::loader_list) .service(tags::loader_create) .service(super::tags::loader_delete) - .service(super::tags::game_version_list) + .service(tags::game_version_list) .service(super::tags::game_version_create) .service(super::tags::game_version_delete) .service(super::tags::license_create) @@ -93,14 +94,14 @@ pub fn users_config(cfg: &mut web::ServiceConfig) { .service(super::users::user_edit) .service(super::users::user_icon_edit) .service(super::users::user_notifications) - .service(super::users::user_follows), + .service(users::user_follows), ); } pub fn teams_config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("team") - .service(super::teams::team_members_get) + .service(teams::team_members_get) .service(super::teams::edit_team_member) .service(super::teams::add_team_member) .service(super::teams::join_team) diff --git a/src/routes/v1/tags.rs b/src/routes/v1/tags.rs index f5482e654..cbc8fd859 100644 --- a/src/routes/v1/tags.rs +++ b/src/routes/v1/tags.rs @@ -1,5 +1,5 @@ use crate::auth::check_is_admin_from_headers; -use crate::database::models::categories::{Category, Loader, ProjectType}; +use crate::database::models::categories::{Category, GameVersion, Loader, ProjectType}; use crate::routes::ApiError; use actix_web::{get, put, web}; use actix_web::{HttpRequest, HttpResponse}; @@ -79,3 +79,32 @@ pub async fn loader_create( Ok(HttpResponse::NoContent().body("")) } + +#[derive(serde::Deserialize)] +pub struct GameVersionQueryData { + #[serde(rename = "type")] + type_: Option, + major: Option, +} + +#[get("game_version")] +pub async fn game_version_list( + pool: web::Data, + query: web::Query, +) -> Result { + if query.type_.is_some() || query.major.is_some() { + let results = GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool) + .await? + .into_iter() + .map(|x| x.version) + .collect::>(); + Ok(HttpResponse::Ok().json(results)) + } else { + let results = GameVersion::list(&**pool) + .await? + .into_iter() + .map(|x| x.version) + .collect::>(); + Ok(HttpResponse::Ok().json(results)) + } +} diff --git a/src/routes/v1/teams.rs b/src/routes/v1/teams.rs new file mode 100644 index 000000000..4ac474465 --- /dev/null +++ b/src/routes/v1/teams.rs @@ -0,0 +1,76 @@ +use crate::auth::get_user_from_headers; +use crate::models::teams::{Permissions, TeamId}; +use crate::models::users::UserId; +use crate::routes::ApiError; +use actix_web::{get, web, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +/// A member of a team +#[derive(Serialize, Deserialize, Clone)] +pub struct TeamMember { + /// The ID of the team this team member is a member of + pub team_id: TeamId, + /// The ID of the user associated with the member + pub user_id: UserId, + /// The role of the user in the team + pub role: String, + /// A bitset containing the user's permissions in this team + pub permissions: Option, + /// Whether the user has joined the team or is just invited to it + pub accepted: bool, +} + +#[get("{id}/members")] +pub async fn team_members_get( + req: HttpRequest, + info: web::Path<(TeamId,)>, + pool: web::Data, +) -> Result { + let id = info.into_inner().0; + let members_data = + crate::database::models::TeamMember::get_from_team(id.into(), &**pool).await?; + + let current_user = get_user_from_headers(req.headers(), &**pool).await.ok(); + + if let Some(user) = current_user { + let team_member = crate::database::models::TeamMember::get_from_user_id( + id.into(), + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)?; + + if team_member.is_some() { + let team_members: Vec = members_data + .into_iter() + .map(|data| TeamMember { + team_id: id, + user_id: data.user_id.into(), + role: data.role, + permissions: Some(data.permissions), + accepted: data.accepted, + }) + .collect(); + + return Ok(HttpResponse::Ok().json(team_members)); + } + } + + let mut team_members: Vec = Vec::new(); + + for team_member in members_data { + if team_member.accepted { + team_members.push(TeamMember { + team_id: id, + user_id: team_member.user_id.into(), + role: team_member.role, + permissions: None, + accepted: team_member.accepted, + }) + } + } + + Ok(HttpResponse::Ok().json(team_members)) +} diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs index b0a5d38ff..a191067c0 100644 --- a/src/routes/v1/users.rs +++ b/src/routes/v1/users.rs @@ -1,7 +1,7 @@ use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::models::ids::UserId; -use crate::models::projects::ProjectStatus; +use crate::models::projects::{ProjectId, ProjectStatus}; use crate::routes::ApiError; use actix_web::web; use actix_web::{get, HttpRequest, HttpResponse}; @@ -42,3 +42,41 @@ pub async fn mods_list( Ok(HttpResponse::NotFound().body("")) } } + +#[get("{id}/follows")] +pub async fn user_follows( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; + + if let Some(id) = id_option { + if !user.role.is_mod() && user.id != id.into() { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to see the projects this user follows!".to_string(), + )); + } + + use futures::TryStreamExt; + + let projects: Vec = sqlx::query!( + " + SELECT mf.mod_id FROM mod_follows mf + WHERE mf.follower_id = $1 + ", + id as crate::database::models::ids::UserId, + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) }) + .try_collect::>() + .await?; + + Ok(HttpResponse::Ok().json(projects)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 2a44246ba..6d3d7424b 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -1,314 +1,165 @@ -use futures::{StreamExt, TryStreamExt}; +use futures::TryStreamExt; use log::info; use super::IndexingError; -use crate::models::projects::SideType; +use crate::database::models::ProjectId; +use crate::models::projects::ProjectStatus; use crate::search::UploadSearchProject; use sqlx::postgres::PgPool; -use std::borrow::Cow; // TODO: only loaders for recent versions? For projects that have moved from forge to fabric pub async fn index_local(pool: PgPool) -> Result, IndexingError> { info!("Indexing local projects!"); - - let mut docs_to_add: Vec = vec![]; - - let mut projects = sqlx::query!( - " - SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m - " - ).fetch(&pool); - - while let Some(result) = projects.next().await { - if let Ok(project_data) = result { - let status = crate::models::projects::ProjectStatus::from_str( - &sqlx::query!( - " - SELECT status FROM statuses - WHERE id = $1 - ", - project_data.status, - ) - .fetch_one(&pool) - .await? - .status, - ); - - if !status.is_searchable() { - continue; - } - - let versions = sqlx::query!( - " - SELECT DISTINCT gv.version, gv.created FROM versions - INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id - INNER JOIN game_versions gv ON gvv.game_version_id=gv.id - WHERE versions.mod_id = $1 - ORDER BY gv.created ASC - ", - project_data.id - ) + Ok( + sqlx::query!( + " + SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, + m.icon_url icon_url, m.published published, + m.updated updated, + m.team_id team_id, m.license license, m.slug slug, + s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username, + STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions + FROM mods m + LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id + LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id + LEFT OUTER JOIN versions v ON v.mod_id = m.id + INNER JOIN statuses s ON s.id = m.status + INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id + INNER JOIN game_versions gv ON gvv.game_version_id = gv.id + INNER JOIN loaders_versions lv ON lv.version_id = v.id + INNER JOIN loaders lo ON lo.id = lv.loader_id + INNER JOIN project_types pt ON pt.id = m.project_type + INNER JOIN side_types cs ON m.client_side = cs.id + INNER JOIN side_types ss ON m.server_side = ss.id + INNER JOIN licenses l ON m.license = l.id + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $2 + INNER JOIN users u ON tm.user_id = u.id + WHERE s.status = $1 + GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id; + ", + ProjectStatus::Approved.as_str(), + crate::models::teams::OWNER_ROLE, + ) .fetch_many(&pool) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() - .await?; + .try_filter_map(|e| async { + Ok(e.right().map(|m| { + let mut categories = m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect::>(); + categories.append(&mut m.loaders.unwrap_or_default().split(',').map(|x| x.to_string()).collect::>()); - let loaders = sqlx::query!( - " - SELECT DISTINCT loaders.loader FROM versions - INNER JOIN loaders_versions lv ON lv.version_id = versions.id - INNER JOIN loaders ON loaders.id = lv.loader_id - WHERE versions.mod_id = $1 - ", - project_data.id - ) - .fetch_many(&pool) - .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) }) - .try_collect::>>() - .await?; + let versions : Vec = m.versions.unwrap_or_default().split(',').map(|x| x.to_string()).collect::>(); - let mut categories = sqlx::query!( - " - SELECT c.category - FROM mods_categories mc - INNER JOIN categories c ON mc.joining_category_id=c.id - WHERE mc.joining_mod_id = $1 - ", - project_data.id - ) - .fetch_many(&pool) - .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) }) - .try_collect::>>() - .await?; + let project_id : crate::models::projects::ProjectId = ProjectId(m.id).into(); - categories.extend(loaders); - - let user = sqlx::query!( - " - SELECT u.id, u.username FROM users u - INNER JOIN team_members tm ON tm.user_id = u.id - WHERE tm.team_id = $2 AND tm.role = $1 - ", - crate::models::teams::OWNER_ROLE, - project_data.team_id, - ) - .fetch_one(&pool) - .await?; - - let mut icon_url = "".to_string(); - - if let Some(url) = project_data.icon_url { - icon_url = url; - } - - let project_id = crate::models::ids::ProjectId(project_data.id as u64); - - // TODO: is this correct? This just gets the latest version of - // minecraft that this project has a version that supports; it doesn't - // take betas or other info into account. - let latest_version = versions - .last() - .cloned() - .map(Cow::Owned) - .unwrap_or_else(|| Cow::Borrowed("")); - - let client_side = SideType::from_str( - &sqlx::query!( - " - SELECT name FROM side_types - WHERE id = $1 - ", - project_data.client_side, - ) - .fetch_one(&pool) - .await? - .name, - ); - - let server_side = SideType::from_str( - &sqlx::query!( - " - SELECT name FROM side_types - WHERE id = $1 - ", - project_data.server_side, - ) - .fetch_one(&pool) - .await? - .name, - ); - - let license = crate::database::models::categories::License::get( - crate::database::models::LicenseId(project_data.license), - &pool, - ) - .await?; - - docs_to_add.push(UploadSearchProject { - project_id: format!("local-{}", project_id), - title: project_data.title, - description: project_data.description, - categories, - versions, - follows: project_data.follows, - downloads: project_data.downloads, - icon_url, - author: user.username, - date_created: project_data.published, - created_timestamp: project_data.published.timestamp(), - date_modified: project_data.updated, - modified_timestamp: project_data.updated.timestamp(), - latest_version, - license: license.short, - client_side: client_side.to_string(), - server_side: server_side.to_string(), - host: Cow::Borrowed("modrinth"), - slug: project_data.slug, - }); - } - } - - Ok(docs_to_add) + UploadSearchProject { + project_id: format!("{}", project_id), + title: m.title, + description: m.description, + categories, + follows: m.follows, + downloads: m.downloads, + icon_url: m.icon_url.unwrap_or_default(), + author: m.username, + date_created: m.published, + created_timestamp: m.published.timestamp(), + date_modified: m.updated, + modified_timestamp: m.updated.timestamp(), + latest_version: versions.last().cloned().unwrap_or_else(|| "None".to_string()), + versions, + license: m.short, + client_side: m.client_side_type, + server_side: m.server_side_type, + slug: m.slug, + project_type: m.project_type_name, + } + })) + }) + .try_collect::>() + .await? + ) } pub async fn query_one( - id: crate::database::models::ProjectId, + id: ProjectId, exec: &mut sqlx::PgConnection, ) -> Result { - let project_data = sqlx::query!( - " - SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side - FROM mods m - WHERE id = $1 - ", - id.0, - ).fetch_one(&mut *exec).await?; - - let versions = sqlx::query!( - " - SELECT DISTINCT gv.version, gv.created FROM versions - INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id - INNER JOIN game_versions gv ON gvv.game_version_id=gv.id - WHERE versions.mod_id = $1 - ORDER BY gv.created ASC - ", - project_data.id - ) - .fetch_many(&mut *exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() - .await?; - - let loaders = sqlx::query!( - " - SELECT DISTINCT loaders.loader FROM versions - INNER JOIN loaders_versions lv ON lv.version_id = versions.id - INNER JOIN loaders ON loaders.id = lv.loader_id - WHERE versions.mod_id = $1 - ", - project_data.id - ) - .fetch_many(&mut *exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) }) - .try_collect::>>() - .await?; - - let mut categories = sqlx::query!( - " - SELECT c.category - FROM mods_categories mc - INNER JOIN categories c ON mc.joining_category_id=c.id - WHERE mc.joining_mod_id = $1 - ", - project_data.id - ) - .fetch_many(&mut *exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) }) - .try_collect::>>() - .await?; - - categories.extend(loaders); - - let user = sqlx::query!( - " - SELECT u.id, u.username FROM users u - INNER JOIN team_members tm ON tm.user_id = u.id - WHERE tm.team_id = $2 AND tm.role = $1 - ", - crate::models::teams::OWNER_ROLE, - project_data.team_id, - ) - .fetch_one(&mut *exec) - .await?; - - let mut icon_url = "".to_string(); - - if let Some(url) = project_data.icon_url { - icon_url = url; - } - - let project_id = crate::models::ids::ProjectId(project_data.id as u64); - - // TODO: is this correct? This just gets the latest version of - // minecraft that this project has a version that supports; it doesn't - // take betas or other info into account. - let latest_version = versions - .last() - .cloned() - .map(Cow::Owned) - .unwrap_or_else(|| Cow::Borrowed("")); - - let client_side = SideType::from_str( - &sqlx::query!( + let m = sqlx::query!( " - SELECT name FROM side_types - WHERE id = $1 + SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, + m.icon_url icon_url, m.published published, + m.updated updated, + m.team_id team_id, m.license license, m.slug slug, + s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username, + STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions + FROM mods m + LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id + LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id + LEFT OUTER JOIN versions v ON v.mod_id = m.id + INNER JOIN statuses s ON s.id = m.status + INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id + INNER JOIN game_versions gv ON gvv.game_version_id = gv.id + INNER JOIN loaders_versions lv ON lv.version_id = v.id + INNER JOIN loaders lo ON lo.id = lv.loader_id + INNER JOIN project_types pt ON pt.id = m.project_type + INNER JOIN side_types cs ON m.client_side = cs.id + INNER JOIN side_types ss ON m.server_side = ss.id + INNER JOIN licenses l ON m.license = l.id + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $2 + INNER JOIN users u ON tm.user_id = u.id + WHERE m.id = $1 + GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id; ", - project_data.client_side, + id as ProjectId, + crate::models::teams::OWNER_ROLE, ) - .fetch_one(&mut *exec) - .await? - .name, + .fetch_one(exec) + .await?; + + let mut categories = m + .categories + .unwrap_or_default() + .split(',') + .map(|x| x.to_string()) + .collect::>(); + categories.append( + &mut m + .loaders + .unwrap_or_default() + .split(',') + .map(|x| x.to_string()) + .collect::>(), ); - let server_side = SideType::from_str( - &sqlx::query!( - " - SELECT name FROM side_types - WHERE id = $1 - ", - project_data.server_side, - ) - .fetch_one(&mut *exec) - .await? - .name, - ); + let versions: Vec = m + .versions + .unwrap_or_default() + .split(',') + .map(|x| x.to_string()) + .collect::>(); - let license = crate::database::models::categories::License::get( - crate::database::models::LicenseId(project_data.license), - &mut *exec, - ) - .await?; + let project_id: crate::models::projects::ProjectId = ProjectId(m.id).into(); Ok(UploadSearchProject { - project_id: format!("local-{}", project_id), - title: project_data.title, - description: project_data.description, + project_id: format!("{}", project_id), + title: m.title, + description: m.description, categories, + follows: m.follows, + downloads: m.downloads, + icon_url: m.icon_url.unwrap_or_default(), + author: m.username, + date_created: m.published, + created_timestamp: m.published.timestamp(), + date_modified: m.updated, + modified_timestamp: m.updated.timestamp(), + latest_version: versions + .last() + .cloned() + .unwrap_or_else(|| "None".to_string()), versions, - follows: project_data.follows, - downloads: project_data.downloads, - icon_url, - author: user.username, - date_created: project_data.published, - created_timestamp: project_data.published.timestamp(), - date_modified: project_data.updated, - modified_timestamp: project_data.updated.timestamp(), - latest_version, - license: license.short, - client_side: client_side.to_string(), - server_side: server_side.to_string(), - host: Cow::Borrowed("modrinth"), - slug: project_data.slug, + license: m.short, + client_side: m.client_side_type, + server_side: m.server_side_type, + slug: m.slug, + project_type: m.project_type_name, }) } diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index f3b4daa78..61fef63ea 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -264,6 +264,7 @@ fn default_rules() -> VecDeque { fn default_settings() -> Settings { let displayed_attributes = vec![ "project_id".to_string(), + "project_type".to_string(), "slug".to_string(), "author".to_string(), "title".to_string(), diff --git a/src/search/mod.rs b/src/search/mod.rs index 9a1cb6d53..b5165c4d9 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -62,16 +62,17 @@ pub struct SearchConfig { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UploadSearchProject { pub project_id: String, + pub project_type: String, pub slug: Option, pub author: String, pub title: String, pub description: String, - pub categories: Vec>, + pub categories: Vec, pub versions: Vec, pub follows: i32, pub downloads: i32, pub icon_url: String, - pub latest_version: Cow<'static, str>, + pub latest_version: String, pub license: String, pub client_side: String, pub server_side: String, @@ -84,8 +85,6 @@ pub struct UploadSearchProject { pub date_modified: DateTime, /// Unix timestamp of the last major modification pub modified_timestamp: i64, - - pub host: Cow<'static, str>, } #[derive(Serialize, Deserialize, Debug)] @@ -99,6 +98,7 @@ pub struct SearchResults { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ResultSearchProject { pub project_id: String, + pub project_type: String, pub slug: Option, pub author: String, pub title: String, diff --git a/src/validate/fabric.rs b/src/validate/fabric.rs index b15066c0e..7e3b3ac2b 100644 --- a/src/validate/fabric.rs +++ b/src/validate/fabric.rs @@ -30,7 +30,11 @@ impl super::Validator for FabricValidator { &self, archive: &mut ZipArchive>, ) -> Result { - archive.by_name("fabric.mod.json")?; + archive.by_name("fabric.mod.json").map_err(|_| { + ValidationError::InvalidInputError( + "No fabric.mod.json present for Fabric file.".to_string(), + ) + })?; if !archive .file_names() diff --git a/src/validate/forge.rs b/src/validate/forge.rs index bd0367649..3442268c1 100644 --- a/src/validate/forge.rs +++ b/src/validate/forge.rs @@ -30,7 +30,9 @@ impl super::Validator for ForgeValidator { &self, archive: &mut ZipArchive>, ) -> Result { - archive.by_name("META-INF/mods.toml")?; + archive.by_name("META-INF/mods.toml").map_err(|_| { + ValidationError::InvalidInputError("No mods.toml present for Forge file.".to_string()) + })?; if !archive.file_names().any(|name| name.ends_with(".class")) { return Ok(ValidationResult::Warning( @@ -71,7 +73,9 @@ impl super::Validator for LegacyForgeValidator { &self, archive: &mut ZipArchive>, ) -> Result { - archive.by_name("mcmod.info")?; + archive.by_name("mcmod.info").map_err(|_| { + ValidationError::InvalidInputError("No mcmod.info present for Forge file.".to_string()) + })?; if !archive.file_names().any(|name| name.ends_with(".class")) { return Ok(ValidationResult::Warning( diff --git a/src/validate/pack.rs b/src/validate/pack.rs index 9d857440b..6295b0d3e 100644 --- a/src/validate/pack.rs +++ b/src/validate/pack.rs @@ -78,7 +78,9 @@ impl super::Validator for PackValidator { &self, archive: &mut ZipArchive>, ) -> Result { - let mut file = archive.by_name("index.json")?; + let mut file = archive.by_name("index.json").map_err(|_| { + ValidationError::InvalidInputError("Pack manifest is missing.".to_string()) + })?; let mut contents = String::new(); file.read_to_string(&mut contents)?;