diff --git a/.sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json b/.sqlx/query-108fe88fbd116817e48719c370a63b7d0d56b6de443db0e4466237ee3e93fbde.json similarity index 59% rename from .sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json rename to .sqlx/query-108fe88fbd116817e48719c370a63b7d0d56b6de443db0e4466237ee3e93fbde.json index ffbe1f07..6428c1f7 100644 --- a/.sqlx/query-a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4.json +++ b/.sqlx/query-108fe88fbd116817e48719c370a63b7d0d56b6de443db0e4466237ee3e93fbde.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\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 AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, u.id;\n ", + "query": "\n SELECT m.id id, m.title title, m.description description, m.color color,\n m.icon_url icon_url, m.slug slug,\n u.username username, u.avatar_url avatar_url,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', 0, -- TODO: When webhook is updated to match others, this should match version\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id AND mc.is_additional = FALSE\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 AND v.status != ALL($2)\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE m.id = $1\n GROUP BY m.id, u.id;\n ", "describe": { "columns": [ { @@ -116,5 +116,5 @@ null ] }, - "hash": "a87e276d202a517951a50183adb24306e9fcae87e6eac458e7f813ebb57c78c4" + "hash": "108fe88fbd116817e48719c370a63b7d0d56b6de443db0e4466237ee3e93fbde" } diff --git a/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json b/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json new file mode 100644 index 00000000..aa1d25f4 --- /dev/null +++ b/.sqlx/query-60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e.json @@ -0,0 +1,148 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH version_fields_cte AS (\n SELECT version_id, field_id, int_value, enum_value, string_value\n FROM version_fields WHERE version_id = ANY($1) \n ),\n\t\t\t\tversion_fields_json AS (\n\t\t\t\t\tSELECT DISTINCT version_id,\n JSONB_AGG( \n DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value)\n ) version_fields_json\n FROM version_fields_cte\n GROUP BY version_id\n\t\t\t\t),\n\t\t\t\tloader_fields_cte AS (\n\t\t\t\t\tSELECT DISTINCT vf.version_id, lf.*, l.loader\n\t\t\t\t\tFROM loader_fields lf\n INNER JOIN version_fields_cte vf ON lf.id = vf.field_id\n\t\t\t\t\tLEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id\n\t\t\t\t\tLEFT JOIN loaders l ON lv.loader_id = l.id\n GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader\n\t\t\t\t),\n loader_fields_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', lf.version_id,\n 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional\n )\n ) filter (where lf.id is not null) loader_fields_json\n FROM loader_fields_cte lf\n GROUP BY version_id\n ),\n loader_field_enum_values_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values_json\n FROM loader_field_enum_values lfev\n INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id\n GROUP BY version_id\n ),\n files_cte AS (\n SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type\n FROM files f\n WHERE f.version_id = ANY($1)\n ),\n files_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object('id', id, 'url', url, 'filename', filename, 'primary', is_primary, 'size', size, 'file_type', file_type)\n ) files_json\n FROM files_cte lf\n GROUP BY version_id\n ),\n hashes_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object('algorithm', algorithm, 'hash', encode(hash, 'escape'), 'file_id', file_id)\n ) hashes_json\n FROM hashes\n INNER JOIN files_cte lf on lf.id = hashes.file_id\n GROUP BY version_id\n ),\n dependencies_json AS (\n SELECT DISTINCT dependent_id as version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)\n ) dependencies_json\n FROM dependencies d\n WHERE dependent_id = ANY($1)\n GROUP BY version_id\n )\n\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n f.files_json files,\n h.hashes_json hashes,\n d.dependencies_json dependencies,\n vf.version_fields_json version_fields,\n lf.loader_fields_json loader_fields,\n lfev.loader_field_enum_values_json loader_field_enum_values\n FROM versions v\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN loaders_project_types lpt on l.id = lpt.joining_loader_id\n LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id\n LEFT JOIN games g on lptg.game_id = g.id\n LEFT OUTER JOIN files_json f on v.id = f.version_id\n LEFT OUTER JOIN hashes_json h on v.id = h.version_id\n LEFT OUTER JOIN dependencies_json d on v.id = d.version_id\n LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id\n LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id\n LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id\n WHERE v.id = ANY($1)\n GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, f.files_json, h.hashes_json, d.dependencies_json\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "version_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "version_type", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "featured", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "requested_status", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "ordering", + "type_info": "Int4" + }, + { + "ordinal": 13, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 14, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 15, + "name": "games", + "type_info": "VarcharArray" + }, + { + "ordinal": 16, + "name": "files", + "type_info": "Jsonb" + }, + { + "ordinal": 17, + "name": "hashes", + "type_info": "Jsonb" + }, + { + "ordinal": 18, + "name": "dependencies", + "type_info": "Jsonb" + }, + { + "ordinal": 19, + "name": "version_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 20, + "name": "loader_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 21, + "name": "loader_field_enum_values", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "60072c3c62dd9203107d0827a3bdfafde925a1b3ff46043133ecb78200e6698e" +} diff --git a/.sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json b/.sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json deleted file mode 100644 index 2307d9fd..00000000 --- a/.sqlx/query-8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions,\n JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery,\n JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations\n FROM mods m \n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3)\n LEFT JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT JOIN loaders l on lv.loader_id = l.id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\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", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "approved", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "queued", - "type_info": "Timestamptz" - }, - { - "ordinal": 11, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "requested_status", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 17, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 18, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 19, - "name": "organization_id", - "type_info": "Int8" - }, - { - "ordinal": 20, - "name": "license", - "type_info": "Varchar" - }, - { - "ordinal": 21, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "moderation_message", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "moderation_message_body", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "webhook_sent", - "type_info": "Bool" - }, - { - "ordinal": 25, - "name": "color", - "type_info": "Int4" - }, - { - "ordinal": 26, - "name": "thread_id", - "type_info": "Int8" - }, - { - "ordinal": 27, - "name": "monetization_status", - "type_info": "Varchar" - }, - { - "ordinal": 28, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 29, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 30, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 31, - "name": "categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 32, - "name": "additional_categories", - "type_info": "VarcharArray" - }, - { - "ordinal": 33, - "name": "versions", - "type_info": "Jsonb" - }, - { - "ordinal": 34, - "name": "gallery", - "type_info": "Jsonb" - }, - { - "ordinal": 35, - "name": "donations", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8Array", - "TextArray", - "TextArray" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true, - true, - true, - false, - true, - false, - true, - true, - true, - false, - true, - false, - false, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "8b95bd5ed139be6545147217b2d83f6817ce05fd3212b900f0d437ca42decd47" -} diff --git a/.sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json b/.sqlx/query-c81df8078d6cba7f43a3d47a0b9cd365c7321c9ed304d6dd137cb2198780ff3a.json similarity index 52% rename from .sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json rename to .sqlx/query-c81df8078d6cba7f43a3d47a0b9cd365c7321c9ed304d6dd137cb2198780ff3a.json index 37df9b4a..2a4ab4b5 100644 --- a/.sqlx/query-d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e.json +++ b/.sqlx/query-c81df8078d6cba7f43a3d47a0b9cd365c7321c9ed304d6dd137cb2198780ff3a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', lo.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\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 loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id\n WHERE v.status != ANY($1)\n GROUP BY v.id, m.id, u.id;\n ", + "query": "\n WITH version_fields_cte AS (\n SELECT version_id, field_id, int_value, enum_value, string_value\n FROM version_fields\n ),\n version_fields_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG( \n DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value)\n ) version_fields_json\n FROM version_fields_cte\n GROUP BY version_id\n ),\n loader_fields_cte AS (\n SELECT DISTINCT vf.version_id, lf.*, l.loader\n FROM loader_fields lf\n INNER JOIN version_fields_cte vf ON lf.id = vf.field_id\n LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id\n LEFT JOIN loaders l ON lv.loader_id = l.id\n GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader\n ),\n loader_fields_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', lf.version_id,\n 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional\n )\n ) filter (where lf.id is not null) loader_fields_json\n FROM loader_fields_cte lf\n GROUP BY version_id\n ),\n loader_field_enum_values_json AS (\n SELECT DISTINCT version_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values_json\n FROM loader_field_enum_values lfev\n INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id\n GROUP BY version_id\n )\n\n SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color,\n u.username username,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery,\n vf.version_fields_json version_fields,\n lf.loader_fields_json loader_fields,\n lfev.loader_field_enum_values_json loader_field_enum_values\n FROM versions v\n INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2)\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 loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = lo.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id\n LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id\n LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id\n WHERE v.status != ANY($1)\n GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, m.id, u.id;\n ", "describe": { "columns": [ { @@ -170,5 +170,5 @@ null ] }, - "hash": "d622e6108a96a13d254a489047c7760e1acfa9c41e157f6d75d2538abdba5e4e" + "hash": "c81df8078d6cba7f43a3d47a0b9cd365c7321c9ed304d6dd137cb2198780ff3a" } diff --git a/.sqlx/query-d8946603f5894d2b18ad66f7747134234cd497c928fbbce6f8adef4fe3f3b1df.json b/.sqlx/query-d8946603f5894d2b18ad66f7747134234cd497c928fbbce6f8adef4fe3f3b1df.json new file mode 100644 index 00000000..1634f3f1 --- /dev/null +++ b/.sqlx/query-d8946603f5894d2b18ad66f7747134234cd497c928fbbce6f8adef4fe3f3b1df.json @@ -0,0 +1,252 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH version_fields_cte AS (\n SELECT mod_id, version_id, field_id, int_value, enum_value, string_value\n FROM mods m\n INNER JOIN versions v ON m.id = v.mod_id\n INNER JOIN version_fields vf ON v.id = vf.version_id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ),\n\t\t\t\tversion_fields_json AS (\n\t\t\t\t\tSELECT DISTINCT mod_id,\n JSONB_AGG( \n DISTINCT jsonb_build_object('version_id', version_id, 'field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value)\n ) version_fields_json\n FROM version_fields_cte\n GROUP BY mod_id\n\t\t\t\t),\n\t\t\t\tloader_fields_cte AS (\n\t\t\t\t\tSELECT DISTINCT vf.mod_id, vf.version_id, lf.*, l.loader\n\t\t\t\t\tFROM loader_fields lf\n INNER JOIN version_fields_cte vf ON lf.id = vf.field_id\n\t\t\t\t\tLEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id\n\t\t\t\t\tLEFT JOIN loaders l ON lv.loader_id = l.id\n GROUP BY vf.mod_id, vf.version_id, lf.enum_type, lf.id, l.loader\n\t\t\t\t),\n loader_fields_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'version_id', lf.version_id,\n 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional\n )\n ) filter (where lf.id is not null) loader_fields_json\n FROM loader_fields_cte lf\n GROUP BY mod_id\n ),\n loader_field_enum_values_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values_json\n FROM loader_field_enum_values lfev\n INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id\n GROUP BY mod_id\n ),\n versions_cte AS (\n SELECT DISTINCT mod_id, v.id as id, date_published\n FROM mods m\n INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3)\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ),\n versions_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', id, 'date_published', date_published\n )\n ) filter (where id is not null) versions_json\n FROM versions_cte\n GROUP BY mod_id\n ),\n loaders_cte AS (\n SELECT DISTINCT mod_id, l.id as id, l.loader\n FROM versions_cte\n INNER JOIN loaders_versions lv ON versions_cte.id = lv.version_id\n INNER JOIN loaders l ON lv.loader_id = l.id \n ),\n mods_gallery_json AS (\n SELECT DISTINCT mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering\n )\n ) filter (where image_url is not null) mods_gallery_json\n FROM mods_gallery mg\n INNER JOIN mods m ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY mod_id\n ),\n donations_json AS (\n SELECT DISTINCT joining_mod_id as mod_id,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url\n )\n ) filter (where md.joining_platform_id is not null) donations_json\n FROM mods_donations md\n INNER JOIN mods m ON md.joining_mod_id = m.id AND m.id = ANY($1) OR m.slug = ANY($2)\n INNER JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n GROUP BY mod_id\n )\n \n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories,\n v.versions_json versions,\n mg.mods_gallery_json gallery,\n md.donations_json donations,\n vf.version_fields_json version_fields,\n lf.loader_fields_json loader_fields,\n lfev.loader_field_enum_values_json loader_field_enum_values\n FROM mods m \n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_gallery_json mg ON mg.mod_id = m.id\n LEFT JOIN donations_json md ON md.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions_json v ON v.mod_id = m.id\n LEFT JOIN loaders_cte l on l.mod_id = m.id\n LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id\n LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id\n LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id\n LEFT JOIN games g ON lptg.game_id = g.id\n LEFT OUTER JOIN version_fields_json vf ON m.id = vf.mod_id\n LEFT OUTER JOIN loader_fields_json lf ON m.id = lf.mod_id\n LEFT OUTER JOIN loader_field_enum_values_json lfev ON m.id = lfev.mod_id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id, version_fields_json, loader_fields_json, loader_field_enum_values_json, versions_json, mods_gallery_json, donations_json;\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", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "approved", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "queued", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "requested_status", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 18, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 19, + "name": "organization_id", + "type_info": "Int8" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Varchar" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "moderation_message", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "moderation_message_body", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "webhook_sent", + "type_info": "Bool" + }, + { + "ordinal": 25, + "name": "color", + "type_info": "Int4" + }, + { + "ordinal": 26, + "name": "thread_id", + "type_info": "Int8" + }, + { + "ordinal": 27, + "name": "monetization_status", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "loaders", + "type_info": "VarcharArray" + }, + { + "ordinal": 29, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 30, + "name": "games", + "type_info": "VarcharArray" + }, + { + "ordinal": 31, + "name": "categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 32, + "name": "additional_categories", + "type_info": "VarcharArray" + }, + { + "ordinal": 33, + "name": "versions", + "type_info": "Jsonb" + }, + { + "ordinal": 34, + "name": "gallery", + "type_info": "Jsonb" + }, + { + "ordinal": 35, + "name": "donations", + "type_info": "Jsonb" + }, + { + "ordinal": 36, + "name": "version_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 37, + "name": "loader_fields", + "type_info": "Jsonb" + }, + { + "ordinal": 38, + "name": "loader_field_enum_values", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Int8Array", + "TextArray", + "TextArray" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + true, + true, + true, + true, + true, + true, + false, + true, + false, + true, + true, + true, + false, + true, + false, + false, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "hash": "d8946603f5894d2b18ad66f7747134234cd497c928fbbce6f8adef4fe3f3b1df" +} diff --git a/.sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json b/.sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json deleted file mode 100644 index ae83be3b..00000000 --- a/.sqlx/query-ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.date_published date_published, v.downloads downloads,\n v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering,\n ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,\n JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files,\n JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes,\n JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies,\n \n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'field_id', vf.field_id,\n 'int_value', vf.int_value,\n 'enum_value', vf.enum_value,\n 'string_value', vf.string_value\n )\n ) filter (where vf.field_id is not null) version_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'lf_id', lf.id,\n 'loader_name', l.loader,\n 'field', lf.field,\n 'field_type', lf.field_type,\n 'enum_type', lf.enum_type,\n 'min_val', lf.min_val,\n 'max_val', lf.max_val,\n 'optional', lf.optional\n )\n ) filter (where lf.id is not null) loader_fields,\n JSONB_AGG(\n DISTINCT jsonb_build_object(\n 'id', lfev.id,\n 'enum_id', lfev.enum_id,\n 'value', lfev.value,\n 'ordering', lfev.ordering,\n 'created', lfev.created,\n 'metadata', lfev.metadata\n ) \n ) filter (where lfev.id is not null) loader_field_enum_values\n \n FROM versions v\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN loaders_project_types lpt on l.id = lpt.joining_loader_id\n LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id\n LEFT JOIN games g on lptg.game_id = g.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n LEFT OUTER JOIN version_fields vf on v.id = vf.version_id\n LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id\n LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id\n LEFT OUTER JOIN loader_field_enum_values lfev on lfe.id = lfev.enum_id\n\n WHERE v.id = ANY($1)\n GROUP BY v.id\n ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "version_name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 7, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 8, - "name": "version_type", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "featured", - "type_info": "Bool" - }, - { - "ordinal": 10, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "requested_status", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "ordering", - "type_info": "Int4" - }, - { - "ordinal": 13, - "name": "loaders", - "type_info": "VarcharArray" - }, - { - "ordinal": 14, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 15, - "name": "games", - "type_info": "VarcharArray" - }, - { - "ordinal": 16, - "name": "files", - "type_info": "Jsonb" - }, - { - "ordinal": 17, - "name": "hashes", - "type_info": "Jsonb" - }, - { - "ordinal": 18, - "name": "dependencies", - "type_info": "Jsonb" - }, - { - "ordinal": 19, - "name": "version_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 20, - "name": "loader_fields", - "type_info": "Jsonb" - }, - { - "ordinal": 21, - "name": "loader_field_enum_values", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - null, - null, - null, - null, - null, - null, - null, - null, - null - ] - }, - "hash": "ebf318d2713b9b5b29b19fcc59e0fa8726ea6f4862febc4b650f643393a45cb8" -} diff --git a/migrations/20231130102300_additional_indices.sql b/migrations/20231130102300_additional_indices.sql new file mode 100644 index 00000000..a71138ea --- /dev/null +++ b/migrations/20231130102300_additional_indices.sql @@ -0,0 +1,2 @@ +CREATE INDEX version_fields_version_id ON version_fields (version_id); +CREATE INDEX hashes_file_id ON hashes (file_id); diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 0ce3c7c8..50aab37a 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -227,7 +227,9 @@ pub struct GameId(pub i32); #[sqlx(transparent)] pub struct DonationPlatformId(pub i32); -#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, +)] #[sqlx(transparent)] pub struct VersionId(pub i64); #[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -278,7 +280,9 @@ pub struct SessionId(pub i64); #[sqlx(transparent)] pub struct ImageId(pub i64); -#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[derive( + Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord, +)] #[sqlx(transparent)] pub struct LoaderFieldId(pub i32); diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index 744ea02a..d3b13f09 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::hash::Hasher; use super::ids::*; use super::DatabaseError; @@ -248,14 +249,24 @@ pub struct LoaderFieldEnumValue { pub metadata: serde_json::Value, } -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +impl std::hash::Hash for LoaderFieldEnumValue { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.enum_id.hash(state); + self.value.hash(state); + self.ordering.hash(state); + self.created.hash(state); + } +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] pub struct VersionField { pub version_id: VersionId, pub field_id: LoaderFieldId, pub field_name: String, pub value: VersionFieldValue, } -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] pub enum VersionFieldValue { Integer(i32), Text(String), @@ -796,13 +807,18 @@ impl VersionField { } pub fn from_query_json( - version_id: i64, loader_fields: Option, version_fields: Option, loader_field_enum_values: Option, + allow_many: bool, // If true, will allow multiple values for a single singleton field, returning them as separate VersionFields + // allow_many = true, multiple Bools => two VersionFields of Bool + // allow_many = false, multiple Bools => error + // multiple Arraybools => 1 VersionField of ArrayBool ) -> Vec { #[derive(Deserialize, Debug)] struct JsonLoaderField { + version_id: i64, + lf_id: i32, field: String, field_type: String, @@ -840,13 +856,12 @@ impl VersionField { loader_field_enum_values .and_then(|x| serde_json::from_value(x).ok()) .unwrap_or_default(); - let version_id = VersionId(version_id); query_loader_fields .into_iter() - .filter_map(|q| { + .flat_map(|q| { let loader_field_type = match LoaderFieldType::build(&q.field_type, q.enum_type) { Some(lft) => lft, - None => return None, + None => return vec![], }; let loader_field = LoaderField { id: LoaderFieldId(q.lf_id), @@ -856,6 +871,7 @@ impl VersionField { min_val: q.min_val, max_val: q.max_val, }; + let version_id = VersionId(q.version_id); let values = query_version_field_combined .iter() .filter_map(|qvf| { @@ -883,8 +899,18 @@ impl VersionField { } }) .collect::>(); - - VersionField::build(loader_field, version_id, values).ok() + if allow_many { + VersionField::build_many(loader_field, version_id, values) + .unwrap_or_default() + .into_iter() + .unique() + .collect_vec() + } else { + match VersionField::build(loader_field, version_id, values) { + Ok(vf) => vec![vf], + Err(_) => vec![], + } + } }) .collect() } @@ -902,6 +928,23 @@ impl VersionField { value, }) } + + pub fn build_many( + loader_field: LoaderField, + version_id: VersionId, + query_version_fields: Vec, + ) -> Result, DatabaseError> { + let values = VersionFieldValue::build_many(&loader_field.field_type, query_version_fields)?; + Ok(values + .into_iter() + .map(|value| VersionField { + version_id, + field_id: loader_field.id, + field_name: loader_field.field.clone(), + value, + }) + .collect()) + } } impl VersionFieldValue { @@ -982,25 +1025,56 @@ impl VersionFieldValue { }) } - // Build from internal query data - // This encapsulates reundant behavior in db querie -> object conversions + // This will ensure that if multiple QueryVersionFields are provided, they can be combined into a single VersionFieldValue + // of the appropriate type (ie: false, false, true -> ArrayBoolean([false, false, true])) (and not just Boolean) pub fn build( field_type: &LoaderFieldType, qvfs: Vec, ) -> Result { - let field_name = field_type.to_str(); - let get_first = |qvfs: Vec| -> Result { - if qvfs.len() > 1 { - return Err(DatabaseError::SchemaError(format!( - "Multiple fields for field {}", - field_name - ))); + match field_type { + LoaderFieldType::Integer + | LoaderFieldType::Text + | LoaderFieldType::Boolean + | LoaderFieldType::Enum(_) => { + let mut fields = Self::build_many(field_type, qvfs)?; + if fields.len() > 1 { + return Err(DatabaseError::SchemaError(format!( + "Multiple fields for field {}", + field_type.to_str() + ))); + } + fields.pop().ok_or_else(|| { + DatabaseError::SchemaError(format!( + "No version fields for field {}", + field_type.to_str() + )) + }) } - qvfs.into_iter().next().ok_or_else(|| { - DatabaseError::SchemaError(format!("No version fields for field {}", field_name)) - }) - }; + LoaderFieldType::ArrayInteger + | LoaderFieldType::ArrayText + | LoaderFieldType::ArrayBoolean + | LoaderFieldType::ArrayEnum(_) => { + let fields = Self::build_many(field_type, qvfs)?; + Ok(fields.into_iter().next().ok_or_else(|| { + DatabaseError::SchemaError(format!( + "No version fields for field {}", + field_type.to_str() + )) + })?) + } + } + } + // Build from internal query data + // This encapsulates reundant behavior in db querie -> object conversions + // This allows for multiple fields to be built at once. If there are multiple fields, + // but the type only allows for a single field, then multiple VersionFieldValues will be returned + // If there are multiple fields, and the type allows for multiple fields, then a single VersionFieldValue will be returned (array.len == 1) + pub fn build_many( + field_type: &LoaderFieldType, + qvfs: Vec, + ) -> Result, DatabaseError> { + let field_name = field_type.to_str(); let did_not_exist_error = |field_name: &str, desired_field: &str| { DatabaseError::SchemaError(format!( "Field name {} for field {} in does not exist", @@ -1009,39 +1083,61 @@ impl VersionFieldValue { }; Ok(match field_type { - LoaderFieldType::Integer => VersionFieldValue::Integer( - get_first(qvfs)? - .int_value - .ok_or(did_not_exist_error(field_name, "int_value"))?, - ), - LoaderFieldType::Text => VersionFieldValue::Text( - get_first(qvfs)? - .string_value - .ok_or(did_not_exist_error(field_name, "string_value"))?, - ), - LoaderFieldType::Boolean => VersionFieldValue::Boolean( - get_first(qvfs)? - .int_value - .ok_or(did_not_exist_error(field_name, "int_value"))? - != 0, - ), - LoaderFieldType::ArrayInteger => VersionFieldValue::ArrayInteger( + LoaderFieldType::Integer => qvfs + .into_iter() + .map(|qvf| { + Ok(VersionFieldValue::Integer( + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value"))?, + )) + }) + .collect::, DatabaseError>>()?, + LoaderFieldType::Text => qvfs + .into_iter() + .map(|qvf| { + Ok::(VersionFieldValue::Text( + qvf.string_value + .ok_or(did_not_exist_error(field_name, "string_value"))?, + )) + }) + .collect::, DatabaseError>>()?, + LoaderFieldType::Boolean => qvfs + .into_iter() + .map(|qvf| { + Ok::(VersionFieldValue::Boolean( + qvf.int_value + .ok_or(did_not_exist_error(field_name, "int_value"))? + != 0, + )) + }) + .collect::, DatabaseError>>()?, + LoaderFieldType::Enum(id) => qvfs + .into_iter() + .map(|qvf| { + Ok::(VersionFieldValue::Enum( + *id, + qvf.enum_value + .ok_or(did_not_exist_error(field_name, "enum_value"))?, + )) + }) + .collect::, DatabaseError>>()?, + LoaderFieldType::ArrayInteger => vec![VersionFieldValue::ArrayInteger( qvfs.into_iter() .map(|qvf| { qvf.int_value .ok_or(did_not_exist_error(field_name, "int_value")) }) .collect::>()?, - ), - LoaderFieldType::ArrayText => VersionFieldValue::ArrayText( + )], + LoaderFieldType::ArrayText => vec![VersionFieldValue::ArrayText( qvfs.into_iter() .map(|qvf| { qvf.string_value .ok_or(did_not_exist_error(field_name, "string_value")) }) .collect::>()?, - ), - LoaderFieldType::ArrayBoolean => VersionFieldValue::ArrayBoolean( + )], + LoaderFieldType::ArrayBoolean => vec![VersionFieldValue::ArrayBoolean( qvfs.into_iter() .map(|qvf| { Ok::( @@ -1051,15 +1147,8 @@ impl VersionFieldValue { ) }) .collect::>()?, - ), - - LoaderFieldType::Enum(id) => VersionFieldValue::Enum( - *id, - get_first(qvfs)? - .enum_value - .ok_or(did_not_exist_error(field_name, "enum_value"))?, - ), - LoaderFieldType::ArrayEnum(id) => VersionFieldValue::ArrayEnum( + )], + LoaderFieldType::ArrayEnum(id) => vec![VersionFieldValue::ArrayEnum( *id, qvfs.into_iter() .map(|qvf| { @@ -1067,7 +1156,7 @@ impl VersionFieldValue { .ok_or(did_not_exist_error(field_name, "enum_value")) }) .collect::>()?, - ), + )], }) } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 6be0f01b..c35bdf53 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -1,3 +1,4 @@ +use super::loader_fields::VersionField; use super::{ids::*, User}; use crate::database::models; use crate::database::models::DatabaseError; @@ -565,8 +566,103 @@ impl Project { .map(|x| x as i64) .collect(); + // TODO: Possible improvements to look into: + // - use multiple queries instead of CTES (for cleanliness?) + // - repeated joins to mods in separate CTEs- perhaps 1 CTE for mods and use later (in mods_gallery_json, mods_donations_json, etc.) let db_projects: Vec = sqlx::query!( " + WITH version_fields_cte AS ( + SELECT mod_id, version_id, field_id, int_value, enum_value, string_value + FROM mods m + INNER JOIN versions v ON m.id = v.mod_id + INNER JOIN version_fields vf ON v.id = vf.version_id + WHERE m.id = ANY($1) OR m.slug = ANY($2) + ), + version_fields_json AS ( + SELECT DISTINCT mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object('version_id', version_id, 'field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value) + ) version_fields_json + FROM version_fields_cte + GROUP BY mod_id + ), + loader_fields_cte AS ( + SELECT DISTINCT vf.mod_id, vf.version_id, lf.*, l.loader + FROM loader_fields lf + INNER JOIN version_fields_cte vf ON lf.id = vf.field_id + LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id + LEFT JOIN loaders l ON lv.loader_id = l.id + GROUP BY vf.mod_id, vf.version_id, lf.enum_type, lf.id, l.loader + ), + loader_fields_json AS ( + SELECT DISTINCT mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'version_id', lf.version_id, + 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional + ) + ) filter (where lf.id is not null) loader_fields_json + FROM loader_fields_cte lf + GROUP BY mod_id + ), + loader_field_enum_values_json AS ( + SELECT DISTINCT mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata + ) + ) filter (where lfev.id is not null) loader_field_enum_values_json + FROM loader_field_enum_values lfev + INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id + GROUP BY mod_id + ), + versions_cte AS ( + SELECT DISTINCT mod_id, v.id as id, date_published + FROM mods m + INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3) + WHERE m.id = ANY($1) OR m.slug = ANY($2) + ), + versions_json AS ( + SELECT DISTINCT mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'id', id, 'date_published', date_published + ) + ) filter (where id is not null) versions_json + FROM versions_cte + GROUP BY mod_id + ), + loaders_cte AS ( + SELECT DISTINCT mod_id, l.id as id, l.loader + FROM versions_cte + INNER JOIN loaders_versions lv ON versions_cte.id = lv.version_id + INNER JOIN loaders l ON lv.loader_id = l.id + ), + mods_gallery_json AS ( + SELECT DISTINCT mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering + ) + ) filter (where image_url is not null) mods_gallery_json + FROM mods_gallery mg + INNER JOIN mods m ON mg.mod_id = m.id + WHERE m.id = ANY($1) OR m.slug = ANY($2) + GROUP BY mod_id + ), + donations_json AS ( + SELECT DISTINCT joining_mod_id as mod_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url + ) + ) filter (where md.joining_platform_id is not null) donations_json + FROM mods_donations md + INNER JOIN mods m ON md.joining_mod_id = m.id AND m.id = ANY($1) OR m.slug = ANY($2) + INNER JOIN donation_platforms dp ON md.joining_platform_id = dp.id + GROUP BY mod_id + ) + SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.body body, m.published published, m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status, @@ -579,25 +675,29 @@ impl Project { ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories, ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories, - JSONB_AGG(DISTINCT jsonb_build_object('id', v.id, 'date_published', v.date_published)) filter (where v.id is not null) versions, - JSONB_AGG(DISTINCT jsonb_build_object('image_url', mg.image_url, 'featured', mg.featured, 'title', mg.title, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering)) filter (where mg.image_url is not null) gallery, - JSONB_AGG(DISTINCT jsonb_build_object('platform_id', md.joining_platform_id, 'platform_short', dp.short, 'platform_name', dp.name,'url', md.url)) filter (where md.joining_platform_id is not null) donations + v.versions_json versions, + mg.mods_gallery_json gallery, + md.donations_json donations, + vf.version_fields_json version_fields, + lf.loader_fields_json loader_fields, + lfev.loader_field_enum_values_json loader_field_enum_values FROM mods m INNER JOIN threads t ON t.mod_id = m.id - LEFT JOIN mods_gallery mg ON mg.mod_id = m.id - LEFT JOIN mods_donations md ON md.joining_mod_id = m.id - LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id + LEFT JOIN mods_gallery_json mg ON mg.mod_id = m.id + LEFT JOIN donations_json md ON md.mod_id = m.id LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id LEFT JOIN categories c ON mc.joining_category_id = c.id - LEFT JOIN versions v ON v.mod_id = m.id AND v.status = ANY($3) - LEFT JOIN loaders_versions lv ON lv.version_id = v.id - LEFT JOIN loaders l on lv.loader_id = l.id + LEFT JOIN versions_json v ON v.mod_id = m.id + LEFT JOIN loaders_cte l on l.mod_id = m.id LEFT JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id LEFT JOIN project_types pt ON pt.id = lpt.joining_project_type_id LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id LEFT JOIN games g ON lptg.game_id = g.id + LEFT OUTER JOIN version_fields_json vf ON m.id = vf.mod_id + LEFT OUTER JOIN loader_fields_json lf ON m.id = lf.mod_id + LEFT OUTER JOIN loader_field_enum_values_json lfev ON m.id = lfev.mod_id WHERE m.id = ANY($1) OR m.slug = ANY($2) - GROUP BY t.id, m.id; + GROUP BY t.id, m.id, version_fields_json, loader_fields_json, loader_field_enum_values_json, versions_json, mods_gallery_json, donations_json; ", &project_ids_parsed, &remaining_strings.into_iter().map(|x| x.to_string().to_lowercase()).collect::>(), @@ -677,6 +777,7 @@ impl Project { donation_urls: serde_json::from_value( m.donations.unwrap_or_default(), ).ok().unwrap_or_default(), + aggregate_version_fields: VersionField::from_query_json(m.loader_fields, m.version_fields, m.loader_field_enum_values, true), thread_id: ThreadId(m.thread_id), }})) }) @@ -796,4 +897,5 @@ pub struct QueryProject { pub donation_urls: Vec, pub gallery_items: Vec, pub thread_id: ThreadId, + pub aggregate_version_fields: Vec, } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 3e3fc3ec..96f9fe92 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -528,47 +528,92 @@ impl Version { if !version_ids_parsed.is_empty() { let db_versions: Vec = sqlx::query!( " + WITH version_fields_cte AS ( + SELECT version_id, field_id, int_value, enum_value, string_value + FROM version_fields WHERE version_id = ANY($1) + ), + version_fields_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value) + ) version_fields_json + FROM version_fields_cte + GROUP BY version_id + ), + loader_fields_cte AS ( + SELECT DISTINCT vf.version_id, lf.*, l.loader + FROM loader_fields lf + INNER JOIN version_fields_cte vf ON lf.id = vf.field_id + LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id + LEFT JOIN loaders l ON lv.loader_id = l.id + GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader + ), + loader_fields_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'version_id', lf.version_id, + 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional + ) + ) filter (where lf.id is not null) loader_fields_json + FROM loader_fields_cte lf + GROUP BY version_id + ), + loader_field_enum_values_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata + ) + ) filter (where lfev.id is not null) loader_field_enum_values_json + FROM loader_field_enum_values lfev + INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id + GROUP BY version_id + ), + files_cte AS ( + SELECT DISTINCT version_id, f.id, f.url, f.filename, f.is_primary, f.size, f.file_type + FROM files f + WHERE f.version_id = ANY($1) + ), + files_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object('id', id, 'url', url, 'filename', filename, 'primary', is_primary, 'size', size, 'file_type', file_type) + ) files_json + FROM files_cte lf + GROUP BY version_id + ), + hashes_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object('algorithm', algorithm, 'hash', encode(hash, 'escape'), 'file_id', file_id) + ) hashes_json + FROM hashes + INNER JOIN files_cte lf on lf.id = hashes.file_id + GROUP BY version_id + ), + dependencies_json AS ( + SELECT DISTINCT dependent_id as version_id, + JSONB_AGG( + DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name) + ) dependencies_json + FROM dependencies d + WHERE dependent_id = ANY($1) + GROUP BY version_id + ) + SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number, v.changelog changelog, v.date_published date_published, v.downloads downloads, v.version_type version_type, v.featured featured, v.status status, v.requested_status requested_status, v.ordering ordering, ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, - JSONB_AGG(DISTINCT jsonb_build_object('id', f.id, 'url', f.url, 'filename', f.filename, 'primary', f.is_primary, 'size', f.size, 'file_type', f.file_type)) filter (where f.id is not null) files, - JSONB_AGG(DISTINCT jsonb_build_object('algorithm', h.algorithm, 'hash', encode(h.hash, 'escape'), 'file_id', h.file_id)) filter (where h.hash is not null) hashes, - JSONB_AGG(DISTINCT jsonb_build_object('project_id', d.mod_dependency_id, 'version_id', d.dependency_id, 'dependency_type', d.dependency_type,'file_name', dependency_file_name)) filter (where d.dependency_type is not null) dependencies, - - JSONB_AGG( - DISTINCT jsonb_build_object( - 'field_id', vf.field_id, - 'int_value', vf.int_value, - 'enum_value', vf.enum_value, - 'string_value', vf.string_value - ) - ) filter (where vf.field_id is not null) version_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'lf_id', lf.id, - 'loader_name', l.loader, - 'field', lf.field, - 'field_type', lf.field_type, - 'enum_type', lf.enum_type, - 'min_val', lf.min_val, - 'max_val', lf.max_val, - 'optional', lf.optional - ) - ) filter (where lf.id is not null) loader_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', lfev.id, - 'enum_id', lfev.enum_id, - 'value', lfev.value, - 'ordering', lfev.ordering, - 'created', lfev.created, - 'metadata', lfev.metadata - ) - ) filter (where lfev.id is not null) loader_field_enum_values - + f.files_json files, + h.hashes_json hashes, + d.dependencies_json dependencies, + vf.version_fields_json version_fields, + lf.loader_fields_json loader_fields, + lfev.loader_field_enum_values_json loader_field_enum_values FROM versions v LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id LEFT OUTER JOIN loaders l on lv.loader_id = l.id @@ -576,16 +621,14 @@ impl Version { LEFT JOIN project_types pt on lpt.joining_project_type_id = pt.id LEFT OUTER JOIN loaders_project_types_games lptg on l.id = lptg.loader_id AND pt.id = lptg.project_type_id LEFT JOIN games g on lptg.game_id = g.id - LEFT OUTER JOIN files f on v.id = f.version_id - LEFT OUTER JOIN hashes h on f.id = h.file_id - LEFT OUTER JOIN dependencies d on v.id = d.dependent_id - LEFT OUTER JOIN version_fields vf on v.id = vf.version_id - LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id - LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id - LEFT OUTER JOIN loader_field_enum_values lfev on lfe.id = lfev.enum_id - + LEFT OUTER JOIN files_json f on v.id = f.version_id + LEFT OUTER JOIN hashes_json h on v.id = h.version_id + LEFT OUTER JOIN dependencies_json d on v.id = d.version_id + LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id + LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id + LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id WHERE v.id = ANY($1) - GROUP BY v.id + GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, f.files_json, h.hashes_json, d.dependencies_json ORDER BY v.ordering ASC NULLS LAST, v.date_published ASC; ", &version_ids_parsed @@ -676,7 +719,7 @@ impl Version { files }, - version_fields: VersionField::from_query_json(v.id, v.loader_fields, v.version_fields, v.loader_field_enum_values), + version_fields: VersionField::from_query_json(v.loader_fields, v.version_fields, v.loader_field_enum_values, false), loaders: v.loaders.unwrap_or_default(), project_types: v.project_types.unwrap_or_default(), games: v.games.unwrap_or_default(), diff --git a/src/models/v3/projects.rs b/src/models/v3/projects.rs index 5ed5541e..bad97dd2 100644 --- a/src/models/v3/projects.rs +++ b/src/models/v3/projects.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use super::ids::{Base62Id, OrganizationId}; use super::teams::TeamId; @@ -109,10 +109,49 @@ pub struct Project { /// The monetization status of this project pub monetization_status: MonetizationStatus, + + /// Aggregated loader-fields across its myriad of versions + #[serde(flatten)] + pub fields: HashMap>, +} + +fn remove_duplicates(values: Vec) -> Vec { + let mut seen = HashSet::new(); + values + .into_iter() + .filter(|value| { + // Convert the JSON value to a string for comparison + let as_string = value.to_string(); + // Check if the string is already in the set + seen.insert(as_string) + }) + .collect() } impl From for Project { fn from(data: QueryProject) -> Self { + let mut fields: HashMap> = HashMap::new(); + for vf in data.aggregate_version_fields { + // We use a string directly, so we can remove duplicates + let serialized = if let Some(inner_array) = vf.value.serialize_internal().as_array() { + inner_array.clone() + } else { + vec![vf.value.serialize_internal()] + }; + + // Create array if doesnt exist, otherwise push, or if json is an array, extend + if let Some(arr) = fields.get_mut(&vf.field_name) { + arr.extend(serialized); + } else { + fields.insert(vf.field_name, serialized); + } + } + + // Remove duplicates by converting to string and back + for (_, v) in fields.iter_mut() { + *v = remove_duplicates(v.clone()); + } + let m = data.inner; Self { id: m.id.into(), @@ -196,6 +235,7 @@ impl From for Project { color: m.color, thread_id: data.thread_id.into(), monetization_status: m.monetization_status, + fields, } } } @@ -640,7 +680,7 @@ pub struct VersionFile { /// A dendency which describes what versions are required, break support, or are optional to the /// version's functionality -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Dependency { /// The specific version id that the dependency uses pub version_id: Option, @@ -677,7 +717,7 @@ impl VersionType { } } -#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DependencyType { Required, diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 16bf7f91..63482db2 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -12,7 +12,7 @@ use crate::routes::v3::projects::ProjectIds; use crate::routes::{v2_reroute, v3, ApiError}; use crate::search::{search_for_project, SearchConfig, SearchError}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; -use chrono::{DateTime, Utc}; + use itertools::Itertools; use serde::{Deserialize, Serialize}; use sqlx::PgPool; diff --git a/src/routes/v3/project_creation.rs b/src/routes/v3/project_creation.rs index 2faee36e..659f6dec 100644 --- a/src/routes/v3/project_creation.rs +++ b/src/routes/v3/project_creation.rs @@ -30,6 +30,7 @@ use itertools::Itertools; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; +use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; use validator::Validate; @@ -843,6 +844,7 @@ async fn project_create_inner( color: project_builder.color, thread_id: thread_id.into(), monetization_status: MonetizationStatus::Monetized, + fields: HashMap::new(), // Fields instantiate to empty }; Ok(HttpResponse::Ok().json(response)) diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 6ff4d6db..0d84d932 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -19,6 +19,49 @@ pub async fn index_local( let uploads = sqlx::query!( " + WITH version_fields_cte AS ( + SELECT version_id, field_id, int_value, enum_value, string_value + FROM version_fields + ), + version_fields_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object('field_id', field_id, 'int_value', int_value, 'enum_value', enum_value, 'string_value', string_value) + ) version_fields_json + FROM version_fields_cte + GROUP BY version_id + ), + loader_fields_cte AS ( + SELECT DISTINCT vf.version_id, lf.*, l.loader + FROM loader_fields lf + INNER JOIN version_fields_cte vf ON lf.id = vf.field_id + LEFT JOIN loaders_versions lv ON vf.version_id = lv.version_id + LEFT JOIN loaders l ON lv.loader_id = l.id + GROUP BY vf.version_id, lf.enum_type, lf.id, l.loader + ), + loader_fields_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'version_id', lf.version_id, + 'lf_id', id, 'loader_name', loader, 'field', field, 'field_type', field_type, 'enum_type', enum_type, 'min_val', min_val, 'max_val', max_val, 'optional', optional + ) + ) filter (where lf.id is not null) loader_fields_json + FROM loader_fields_cte lf + GROUP BY version_id + ), + loader_field_enum_values_json AS ( + SELECT DISTINCT version_id, + JSONB_AGG( + DISTINCT jsonb_build_object( + 'id', lfev.id, 'enum_id', lfev.enum_id, 'value', lfev.value, 'ordering', lfev.ordering, 'created', lfev.created, 'metadata', lfev.metadata + ) + ) filter (where lfev.id is not null) loader_field_enum_values_json + FROM loader_field_enum_values lfev + INNER JOIN loader_fields_cte lf on lf.enum_type = lfev.enum_id + GROUP BY version_id + ) + SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.published published, m.approved approved, m.updated updated, m.team_id team_id, m.license license, m.slug slug, m.status status_name, m.color color, @@ -30,37 +73,9 @@ pub async fn index_local( ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is false) gallery, ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null and mg.featured is true) featured_gallery, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'field_id', vf.field_id, - 'int_value', vf.int_value, - 'enum_value', vf.enum_value, - 'string_value', vf.string_value - ) - ) filter (where vf.field_id is not null) version_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'lf_id', lf.id, - 'loader_name', lo.loader, - 'field', lf.field, - 'field_type', lf.field_type, - 'enum_type', lf.enum_type, - 'min_val', lf.min_val, - 'max_val', lf.max_val, - 'optional', lf.optional - ) - ) filter (where lf.id is not null) loader_fields, - JSONB_AGG( - DISTINCT jsonb_build_object( - 'id', lfev.id, - 'enum_id', lfev.enum_id, - 'value', lfev.value, - 'ordering', lfev.ordering, - 'created', lfev.created, - 'metadata', lfev.metadata - ) - ) filter (where lfev.id is not null) loader_field_enum_values - + vf.version_fields_json version_fields, + lf.loader_fields_json loader_fields, + lfev.loader_field_enum_values_json loader_field_enum_values FROM versions v INNER JOIN mods m ON v.mod_id = m.id AND m.status = ANY($2) LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id @@ -74,12 +89,11 @@ pub async fn index_local( LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE INNER JOIN users u ON tm.user_id = u.id - LEFT OUTER JOIN version_fields vf on v.id = vf.version_id - LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id - LEFT OUTER JOIN loader_field_enums lfe on lf.enum_type = lfe.id - LEFT OUTER JOIN loader_field_enum_values lfev on lfev.enum_id = lfe.id + LEFT OUTER JOIN version_fields_json vf ON v.id = vf.version_id + LEFT OUTER JOIN loader_fields_json lf ON v.id = lf.version_id + LEFT OUTER JOIN loader_field_enum_values_json lfev ON v.id = lfev.version_id WHERE v.status != ANY($1) - GROUP BY v.id, m.id, u.id; + GROUP BY v.id, vf.version_fields_json, lf.loader_fields_json, lfev.loader_field_enum_values_json, m.id, u.id; ", &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::>(), @@ -98,7 +112,7 @@ pub async fn index_local( let display_categories = categories.clone(); categories.append(&mut additional_categories); - let version_fields = VersionField::from_query_json(m.id, m.loader_fields, m.version_fields, m.loader_field_enum_values); + let version_fields = VersionField::from_query_json(m.loader_fields, m.version_fields, m.loader_field_enum_values, false); let loader_fields : HashMap> = version_fields.into_iter().map(|vf| { (vf.field_name, vf.value.as_strings()) diff --git a/src/util/webhook.rs b/src/util/webhook.rs index 11039949..ff3cd9f7 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -104,6 +104,7 @@ pub async fn send_discord_webhook( ) filter (where vf.field_id is not null) version_fields, JSONB_AGG( DISTINCT jsonb_build_object( + 'version_id', 0, -- TODO: When webhook is updated to match others, this should match version 'lf_id', lf.id, 'loader_name', lo.loader, 'field', lf.field, @@ -227,10 +228,10 @@ pub async fn send_discord_webhook( // TODO: Modified to keep "Versions" as a field as it may be hardcoded. Ideally, this pushes all loader fields to the embed for v3 // TODO: This might need some work to manually test let version_fields = VersionField::from_query_json( - project.id, project.loader_fields, project.version_fields, project.loader_field_enum_values, + true, ); let versions = version_fields .into_iter() diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index 73981a64..d2bd49ea 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -8,7 +8,7 @@ use actix_web::{ use async_trait::async_trait; use bytes::Bytes; use chrono::{DateTime, Utc}; -use labrinth::{search::SearchResults, util::actix::AppendsMultipart}; +use labrinth::{models::projects::Project, search::SearchResults, util::actix::AppendsMultipart}; use rust_decimal::Decimal; use serde_json::json; @@ -182,6 +182,12 @@ impl ApiProject for ApiV3 { } impl ApiV3 { + pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project { + let resp = self.get_project(id_or_slug, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + pub async fn search_deserialized( &self, query: Option<&str>, diff --git a/tests/loader_fields.rs b/tests/loader_fields.rs index 03e0925c..d4ed8790 100644 --- a/tests/loader_fields.rs +++ b/tests/loader_fields.rs @@ -13,6 +13,7 @@ use crate::common::dummy_data::TestFile; mod common; #[actix_rt::test] + async fn creating_loader_fields() { with_test_environment(None, |test_env: TestEnvironment| async move { let api = &test_env.api; @@ -24,7 +25,18 @@ async fn creating_loader_fields() { .project_alpha .project_id .clone(); - let alpha_project_id = serde_json::from_str(&format!("\"{}\"", alpha_project_id)).unwrap(); + let alpha_project_id_parsed = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id_parsed; + let beta_project_id_parsed = test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id_parsed; let alpha_version_id = &test_env .dummy .as_ref() @@ -39,7 +51,7 @@ async fn creating_loader_fields() { // - Create version let resp = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -72,7 +84,7 @@ async fn creating_loader_fields() { // - Create version let resp = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -105,7 +117,7 @@ async fn creating_loader_fields() { // - Create version let resp = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -127,7 +139,7 @@ async fn creating_loader_fields() { // - Create version let resp = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -149,7 +161,7 @@ async fn creating_loader_fields() { // - Create version let resp: actix_web::dev::ServiceResponse = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -189,7 +201,7 @@ async fn creating_loader_fields() { // - Create version let resp = api .add_public_version( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -224,7 +236,7 @@ async fn creating_loader_fields() { // - Create version let v = api .add_public_version_deserialized( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -260,7 +272,7 @@ async fn creating_loader_fields() { // - Create let v = api .add_public_version_deserialized( - alpha_project_id, + alpha_project_id_parsed, "1.0.0", TestFile::build_random_jar(), None, @@ -309,6 +321,59 @@ async fn creating_loader_fields() { v.fields.get("game_versions").unwrap(), &json!(["1.20.1", "1.20.2"]) ); + + // Now that we've created a version, we need to make sure that the Project's loader fields are updated (aggregate) + // First, add a new version + api.add_public_version_deserialized( + alpha_project_id_parsed, + "1.0.1", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/game_versions", + "value": ["1.20.5"] + }, { + "op": "add", + "path": "/singleplayer", + "value": false + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + + // Also, add one to the beta project + api.add_public_version_deserialized( + beta_project_id_parsed, + "1.0.1", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/game_versions", + "value": ["1.20.4"] + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + + let project = api + .get_project_deserialized(&alpha_project_id.to_string(), USER_USER_PAT) + .await; + assert_eq!( + project.fields.get("game_versions").unwrap(), + &[json!("1.20.1"), json!("1.20.2"), json!("1.20.5")] + ); + assert_eq!( + project.fields.get("singleplayer").unwrap(), + &[json!(false), json!(true)] + ); }) .await } diff --git a/tests/version.rs b/tests/version.rs index dc559d7c..97bb4980 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -13,7 +13,9 @@ use common::environment::{with_test_environment, with_test_environment_all}; use futures::StreamExt; use labrinth::database::models::version_item::VERSIONS_NAMESPACE; use labrinth::models::ids::base62_impl::parse_base62; -use labrinth::models::projects::{VersionId, VersionStatus, VersionType}; +use labrinth::models::projects::{ + Dependency, DependencyType, VersionId, VersionStatus, VersionType, +}; use labrinth::routes::v3::version_file::FileUpdateData; use serde_json::json; @@ -381,6 +383,13 @@ pub async fn test_patch_version() { let api = &test_env.api; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; + let beta_project_id_parsed = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id_parsed; // // First, we do some patch requests that should fail. // // Failure because the user is not authorized. @@ -419,7 +428,11 @@ pub async fn test_patch_version() { "version_number": "1.3.0", "changelog": "new changelog", "version_type": "beta", - // // "dependencies": [], TODO: test this + "dependencies": [{ + "project_id": beta_project_id, + "dependency_type": "required", + "file_name": "dummy_file_name" + }], "game_versions": ["1.20.5"], "loaders": ["forge"], "featured": false, @@ -443,6 +456,15 @@ pub async fn test_patch_version() { version.version_type, serde_json::from_str::("\"beta\"").unwrap() ); + assert_eq!( + version.dependencies, + vec![Dependency { + project_id: Some(*beta_project_id_parsed), + version_id: None, + file_name: Some("dummy_file_name".to_string()), + dependency_type: DependencyType::Required + }] + ); assert_eq!(version.loaders, vec!["forge".to_string()]); assert!(!version.featured); assert_eq!(version.status, VersionStatus::from_string("draft"));