You've already forked AstralRinth
forked from didirus/AstralRinth
Staging bug fixes (#819)
* Staging bug fixes * Finish fixes * fix tests * Update migration * Update migrations * fix side types being added for ineligible loaders * fix tests * Fix tests * Finish fixes * Add slug display names
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ",
|
"query": "\n INSERT INTO team_members (\n id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted, payouts_split\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n )\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -12,10 +12,11 @@
|
|||||||
"Int8",
|
"Int8",
|
||||||
"Int8",
|
"Int8",
|
||||||
"Bool",
|
"Bool",
|
||||||
"Bool"
|
"Bool",
|
||||||
|
"Numeric"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "8d6166509c910b6400efc1d1398aae92ebe04b0d4d1fe1762208167cee23b645"
|
"hash": "02a585c845168c1bd8a82c30af351db75597da5456e29efc033ebb098e81e905"
|
||||||
}
|
}
|
||||||
20
.sqlx/query-0c2addb0d7a87fa558821ff8e943bbb751fb2bdc22d1a5368f61cc7827586840.json
generated
Normal file
20
.sqlx/query-0c2addb0d7a87fa558821ff8e943bbb751fb2bdc22d1a5368f61cc7827586840.json
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Varchar",
|
||||||
|
"Varchar",
|
||||||
|
"Bool",
|
||||||
|
"Int4",
|
||||||
|
"Varchar"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "0c2addb0d7a87fa558821ff8e943bbb751fb2bdc22d1a5368f61cc7827586840"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, \n $7, $8, $9, $10, \n $11, $12, \n LOWER($13), $14, $15\n )\n ",
|
"query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, \n $7, $8, $9, $10, \n $11, $12, \n LOWER($13), $14, $15, $16\n )\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -19,10 +19,11 @@
|
|||||||
"Varchar",
|
"Varchar",
|
||||||
"Text",
|
"Text",
|
||||||
"Int4",
|
"Int4",
|
||||||
"Varchar"
|
"Varchar",
|
||||||
|
"Int8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "9b0c04bc7d44a60c175259cfe86b8e7ba0340ea9c89be30a89cc224d0f7e9727"
|
"hash": "177b15719778b7788b88877af6affb8dba11da318b14dab7fcc7165c46bbecf5"
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)\n SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::varchar[], $4::varchar[], $5::bool[], $6::integer[], $7::varchar[])\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8Array",
|
|
||||||
"Int8Array",
|
|
||||||
"VarcharArray",
|
|
||||||
"VarcharArray",
|
|
||||||
"BoolArray",
|
|
||||||
"Int4Array",
|
|
||||||
"VarcharArray"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "24ae57ca296554a29b414caca866cfe7ab956ea28450d40a564498c3d27b937f"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT EXISTS(SELECT 1 FROM organizations WHERE name = LOWER($1))\n ",
|
"query": "\n SELECT EXISTS(SELECT 1 FROM organizations WHERE LOWER(slug) = LOWER($1))\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -18,5 +18,5 @@
|
|||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "da30019590b9d0f7e21668997e780044c67a7c5d225e556c7ec2a4d7709db5ea"
|
"hash": "38f651362c0778254c28ccd4745af611f4deb6e72f52b8cf65d0515f0fe14779"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.name) = ANY($2)\n GROUP BY o.id;\n ",
|
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)\n GROUP BY o.id;\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -10,26 +10,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 1,
|
"ordinal": 1,
|
||||||
"name": "name",
|
"name": "slug",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
"name": "team_id",
|
"name": "team_id",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 4,
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 5,
|
||||||
"name": "icon_url",
|
"name": "icon_url",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 6,
|
||||||
"name": "color",
|
"name": "color",
|
||||||
"type_info": "Int4"
|
"type_info": "Int4"
|
||||||
}
|
}
|
||||||
@@ -45,9 +50,10 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "ca9f3298ff92051412f5096690b3314fe91fe0b7c79ab2f7d09396af47b85ee6"
|
"hash": "4deaf065c12dbfd5f585286001fdf66f60524ec13eab7d922db9290237297849"
|
||||||
}
|
}
|
||||||
20
.sqlx/query-a004ad357abfc01b4ab8a2e0a78e2b38f8a0edb7e3b2174d040ac4bb6e5bde39.json
generated
Normal file
20
.sqlx/query-a004ad357abfc01b4ab8a2e0a78e2b38f8a0edb7e3b2174d040ac4bb6e5bde39.json
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO organizations (id, slug, name, team_id, description, icon_url, color)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Varchar",
|
||||||
|
"Text",
|
||||||
|
"Int8",
|
||||||
|
"Text",
|
||||||
|
"Varchar",
|
||||||
|
"Int4"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "a004ad357abfc01b4ab8a2e0a78e2b38f8a0edb7e3b2174d040ac4bb6e5bde39"
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT m.id FROM organizations o\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.name = $2 AND $2 IS NOT NULL)\n ",
|
"query": "\n SELECT m.id FROM organizations o\n INNER JOIN mods m ON m.organization_id = o.id\n WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.slug = $2 AND $2 IS NOT NULL)\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -19,5 +19,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "da962cbb02919ea79e1106e6e5de39224d240d9b8afb5cead28578ca65e281ae"
|
"hash": "a3448f22ec82f75ab2f3769b7d0a653a7d7315fb5e4696c26c6a96e6fc11e907"
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n INSERT INTO organizations (id, name, team_id, description, icon_url, color)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8",
|
|
||||||
"Varchar",
|
|
||||||
"Int8",
|
|
||||||
"Text",
|
|
||||||
"Varchar",
|
|
||||||
"Int4"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "bd48b18b9bef07185d2d050c7c978904cfbdf4ec765b7d3568f930939e236cbe"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n LEFT JOIN mods m ON m.organization_id = o.id\n WHERE m.id = $1\n GROUP BY o.id;\n ",
|
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n LEFT JOIN mods m ON m.organization_id = o.id\n WHERE m.id = $1\n GROUP BY o.id;\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -10,26 +10,31 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 1,
|
"ordinal": 1,
|
||||||
"name": "name",
|
"name": "slug",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 2,
|
"ordinal": 2,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
"name": "team_id",
|
"name": "team_id",
|
||||||
"type_info": "Int8"
|
"type_info": "Int8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 4,
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 4,
|
"ordinal": 5,
|
||||||
"name": "icon_url",
|
"name": "icon_url",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 6,
|
||||||
"name": "color",
|
"name": "color",
|
||||||
"type_info": "Int4"
|
"type_info": "Int4"
|
||||||
}
|
}
|
||||||
@@ -44,9 +49,10 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "26a6271a6d365e64c68ea5855109f1597a121b2e0075b20e2bc34659a269294b"
|
"hash": "c0f23758b879b8a1304ad895b9bcf52b90913f23f96d16c24137b0d529a7475a"
|
||||||
}
|
}
|
||||||
16
.sqlx/query-cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8.json
generated
Normal file
16
.sqlx/query-cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8.json
generated
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO hashes (file_id, algorithm, hash)\n VALUES ($1, $2, $3)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Varchar",
|
||||||
|
"Bytea"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8"
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n INSERT INTO hashes (file_id, algorithm, hash)\n SELECT * FROM UNNEST($1::bigint[], $2::varchar[], $3::bytea[])\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8Array",
|
|
||||||
"VarcharArray",
|
|
||||||
"ByteaArray"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "d2e826d4fa4e3e730cc84c97964c0c5fdd25cd49ddff8c593bd9b8a3b4d5ff1e"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n UPDATE organizations\n SET name = LOWER($1)\n WHERE (id = $2)\n ",
|
"query": "\n UPDATE organizations\n SET name = $1\n WHERE (id = $2)\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -11,5 +11,5 @@
|
|||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "eefe0f3e40273da9adea96cdef5fd5cff917a864a701408455cc6b02cd005cf7"
|
"hash": "dbbfd789feb09459ef25b90eba9458e0d4bceb6389eae13a166556f828a6c3a6"
|
||||||
}
|
}
|
||||||
15
.sqlx/query-ed7cc47dc2acfcaf27c4e763390371dccddbeea902928f1382c9505742f0a9a9.json
generated
Normal file
15
.sqlx/query-ed7cc47dc2acfcaf27c4e763390371dccddbeea902928f1382c9505742f0a9a9.json
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE organizations\n SET slug = $1\n WHERE (id = $2)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ed7cc47dc2acfcaf27c4e763390371dccddbeea902928f1382c9505742f0a9a9"
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ FROM versions v
|
|||||||
INNER JOIN mods m ON v.mod_id = m.id
|
INNER JOIN mods m ON v.mod_id = m.id
|
||||||
INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_id
|
INNER JOIN loader_field_enum_values lfev ON m.client_side = lfev.original_id
|
||||||
CROSS JOIN loader_fields lf
|
CROSS JOIN loader_fields lf
|
||||||
WHERE client_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'client_side';
|
WHERE client_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'client_side' AND NOT (ARRAY['vanilla', 'minecraft', 'optifine', 'iris', 'canvas', 'bukkit', 'folia', 'paper', 'purpur', 'spigot', 'sponge', 'datapack', 'bungeecord', 'velocity', 'waterfall'] @> m.loaders::text[]);;
|
||||||
|
|
||||||
INSERT INTO version_fields (version_id, field_id, enum_value)
|
INSERT INTO version_fields (version_id, field_id, enum_value)
|
||||||
SELECT v.id, lf.id, lfev.id -- Note: bug fix/edited 2023-11-27
|
SELECT v.id, lf.id, lfev.id -- Note: bug fix/edited 2023-11-27
|
||||||
@@ -85,7 +85,7 @@ FROM versions v
|
|||||||
INNER JOIN mods m ON v.mod_id = m.id
|
INNER JOIN mods m ON v.mod_id = m.id
|
||||||
INNER JOIN loader_field_enum_values lfev ON m.server_side = lfev.original_id
|
INNER JOIN loader_field_enum_values lfev ON m.server_side = lfev.original_id
|
||||||
CROSS JOIN loader_fields lf
|
CROSS JOIN loader_fields lf
|
||||||
WHERE server_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'server_side';
|
WHERE server_side IS NOT NULL AND lfev.enum_id = 1 AND lf.field = 'server_side' AND NOT (ARRAY['vanilla', 'minecraft', 'optifine', 'iris', 'canvas', 'bukkit', 'folia', 'paper', 'purpur', 'spigot', 'sponge', 'datapack', 'bungeecord', 'velocity', 'waterfall'] @> m.loaders::text[]);
|
||||||
|
|
||||||
ALTER TABLE mods DROP COLUMN client_side;
|
ALTER TABLE mods DROP COLUMN client_side;
|
||||||
ALTER TABLE mods DROP COLUMN server_side;
|
ALTER TABLE mods DROP COLUMN server_side;
|
||||||
@@ -99,6 +99,10 @@ SELECT id, 2, version, created, json_build_object('type', type, 'major', major)
|
|||||||
INSERT INTO loader_fields (field, field_type, enum_type, optional, min_val) VALUES('game_versions', 'array_enum', 2, false, 0);
|
INSERT INTO loader_fields (field, field_type, enum_type, optional, min_val) VALUES('game_versions', 'array_enum', 2, false, 0);
|
||||||
INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']);
|
INSERT INTO loader_fields_loaders (loader_id, loader_field_id) SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']);
|
||||||
|
|
||||||
|
-- remove dangling game versions
|
||||||
|
DELETE FROM game_versions_versions
|
||||||
|
WHERE joining_version_id NOT IN (SELECT id FROM versions);
|
||||||
|
|
||||||
INSERT INTO version_fields(version_id, field_id, enum_value)
|
INSERT INTO version_fields(version_id, field_id, enum_value)
|
||||||
SELECT gvv.joining_version_id, lf.id, lfev.id
|
SELECT gvv.joining_version_id, lf.id, lfev.id
|
||||||
FROM game_versions_versions gvv INNER JOIN loader_field_enum_values lfev ON gvv.game_version_id = lfev.original_id
|
FROM game_versions_versions gvv INNER JOIN loader_field_enum_values lfev ON gvv.game_version_id = lfev.original_id
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS paypal_email;
|
||||||
|
|
||||||
ALTER TABLE users
|
ALTER TABLE users
|
||||||
ADD COLUMN paypal_country text NULL,
|
ADD COLUMN paypal_country text NULL,
|
||||||
ADD COLUMN paypal_email text NULL,
|
ADD COLUMN paypal_email text NULL,
|
||||||
|
|||||||
@@ -4,9 +4,14 @@
|
|||||||
-- This also allows v2 routes (which have things such as client_side to remain to work with these loaders)
|
-- This also allows v2 routes (which have things such as client_side to remain to work with these loaders)
|
||||||
INSERT INTO loader_fields_loaders
|
INSERT INTO loader_fields_loaders
|
||||||
SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf
|
SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf
|
||||||
WHERE lf.field=ANY(ARRAY['game_versions','client_and_server','server_only','client_only','singleplayer'])
|
WHERE lf.field=ANY(ARRAY['client_and_server','server_only','client_only','singleplayer'])
|
||||||
AND
|
AND
|
||||||
l.loader NOT IN ('vanilla', 'minecraft', 'optifine', 'iris', 'canvas')
|
l.loader NOT IN ('vanilla', 'minecraft', 'optifine', 'iris', 'canvas', 'bukkit', 'folia', 'paper', 'purpur', 'spigot', 'sponge', 'datapack', 'bungeecord', 'velocity', 'waterfall')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO loader_fields_loaders
|
||||||
|
SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf
|
||||||
|
WHERE lf.field=ANY(ARRAY['game_versions'])
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- All existing loader_project_types so far should have a games entry as minecraft
|
-- All existing loader_project_types so far should have a games entry as minecraft
|
||||||
|
|||||||
7
migrations/20240104203711_orgs-names.sql
Normal file
7
migrations/20240104203711_orgs-names.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE organizations RENAME COLUMN name TO slug;
|
||||||
|
|
||||||
|
ALTER TABLE organizations ADD COLUMN name text NULL;
|
||||||
|
UPDATE organizations SET name = slug;
|
||||||
|
ALTER TABLE organizations ALTER COLUMN name SET NOT NULL;
|
||||||
|
|
||||||
@@ -15,7 +15,10 @@ pub struct Organization {
|
|||||||
/// The id of the organization
|
/// The id of the organization
|
||||||
pub id: OrganizationId,
|
pub id: OrganizationId,
|
||||||
|
|
||||||
/// The title (and slug) of the organization
|
/// The slug of the organization
|
||||||
|
pub slug: String,
|
||||||
|
|
||||||
|
/// The title of the organization
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// The associated team of the organization
|
/// The associated team of the organization
|
||||||
@@ -36,10 +39,11 @@ impl Organization {
|
|||||||
) -> Result<(), super::DatabaseError> {
|
) -> Result<(), super::DatabaseError> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO organizations (id, name, team_id, description, icon_url, color)
|
INSERT INTO organizations (id, slug, name, team_id, description, icon_url, color)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
",
|
",
|
||||||
self.id.0,
|
self.id.0,
|
||||||
|
self.slug,
|
||||||
self.name,
|
self.name,
|
||||||
self.team_id as TeamId,
|
self.team_id as TeamId,
|
||||||
self.description,
|
self.description,
|
||||||
@@ -149,7 +153,7 @@ impl Organization {
|
|||||||
{
|
{
|
||||||
remaining_strings.retain(|x| {
|
remaining_strings.retain(|x| {
|
||||||
&to_base62(organization.id.0 as u64) != x
|
&to_base62(organization.id.0 as u64) != x
|
||||||
&& organization.name.to_lowercase() != x.to_lowercase()
|
&& organization.slug.to_lowercase() != x.to_lowercase()
|
||||||
});
|
});
|
||||||
found_organizations.push(organization);
|
found_organizations.push(organization);
|
||||||
continue;
|
continue;
|
||||||
@@ -166,9 +170,9 @@ impl Organization {
|
|||||||
|
|
||||||
let organizations: Vec<Organization> = sqlx::query!(
|
let organizations: Vec<Organization> = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
|
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
|
||||||
FROM organizations o
|
FROM organizations o
|
||||||
WHERE o.id = ANY($1) OR LOWER(o.name) = ANY($2)
|
WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)
|
||||||
GROUP BY o.id;
|
GROUP BY o.id;
|
||||||
",
|
",
|
||||||
&organization_ids_parsed,
|
&organization_ids_parsed,
|
||||||
@@ -181,6 +185,7 @@ impl Organization {
|
|||||||
.try_filter_map(|e| async {
|
.try_filter_map(|e| async {
|
||||||
Ok(e.right().map(|m| Organization {
|
Ok(e.right().map(|m| Organization {
|
||||||
id: OrganizationId(m.id),
|
id: OrganizationId(m.id),
|
||||||
|
slug: m.slug,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
team_id: TeamId(m.team_id),
|
team_id: TeamId(m.team_id),
|
||||||
description: m.description,
|
description: m.description,
|
||||||
@@ -203,7 +208,7 @@ impl Organization {
|
|||||||
redis
|
redis
|
||||||
.set(
|
.set(
|
||||||
ORGANIZATIONS_TITLES_NAMESPACE,
|
ORGANIZATIONS_TITLES_NAMESPACE,
|
||||||
&organization.name.to_lowercase(),
|
&organization.slug.to_lowercase(),
|
||||||
&organization.id.0.to_string(),
|
&organization.id.0.to_string(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -226,7 +231,7 @@ impl Organization {
|
|||||||
{
|
{
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
|
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
|
||||||
FROM organizations o
|
FROM organizations o
|
||||||
LEFT JOIN mods m ON m.organization_id = o.id
|
LEFT JOIN mods m ON m.organization_id = o.id
|
||||||
WHERE m.id = $1
|
WHERE m.id = $1
|
||||||
@@ -240,6 +245,7 @@ impl Organization {
|
|||||||
if let Some(result) = result {
|
if let Some(result) = result {
|
||||||
Ok(Some(Organization {
|
Ok(Some(Organization {
|
||||||
id: OrganizationId(result.id),
|
id: OrganizationId(result.id),
|
||||||
|
slug: result.slug,
|
||||||
name: result.name,
|
name: result.name,
|
||||||
team_id: TeamId(result.team_id),
|
team_id: TeamId(result.team_id),
|
||||||
description: result.description,
|
description: result.description,
|
||||||
@@ -299,7 +305,7 @@ impl Organization {
|
|||||||
|
|
||||||
pub async fn clear_cache(
|
pub async fn clear_cache(
|
||||||
id: OrganizationId,
|
id: OrganizationId,
|
||||||
title: Option<String>,
|
slug: Option<String>,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<(), super::DatabaseError> {
|
) -> Result<(), super::DatabaseError> {
|
||||||
let mut redis = redis.connect().await?;
|
let mut redis = redis.connect().await?;
|
||||||
@@ -309,7 +315,7 @@ impl Organization {
|
|||||||
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
|
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
|
||||||
(
|
(
|
||||||
ORGANIZATIONS_TITLES_NAMESPACE,
|
ORGANIZATIONS_TITLES_NAMESPACE,
|
||||||
title.map(|x| x.to_lowercase()),
|
slug.map(|x| x.to_lowercase()),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -273,13 +273,13 @@ impl Project {
|
|||||||
id, team_id, name, summary, description,
|
id, team_id, name, summary, description,
|
||||||
published, downloads, icon_url, status, requested_status,
|
published, downloads, icon_url, status, requested_status,
|
||||||
license_url, license,
|
license_url, license,
|
||||||
slug, color, monetization_status
|
slug, color, monetization_status, organization_id
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$7, $8, $9, $10,
|
$7, $8, $9, $10,
|
||||||
$11, $12,
|
$11, $12,
|
||||||
LOWER($13), $14, $15
|
LOWER($13), $14, $15, $16
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as ProjectId,
|
self.id as ProjectId,
|
||||||
@@ -297,6 +297,7 @@ impl Project {
|
|||||||
self.slug.as_ref(),
|
self.slug.as_ref(),
|
||||||
self.color.map(|x| x as i32),
|
self.color.map(|x| x as i32),
|
||||||
self.monetization_status.as_str(),
|
self.monetization_status.as_str(),
|
||||||
|
self.organization_id.map(|x| x.0 as i64),
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -412,10 +412,10 @@ impl TeamMember {
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO team_members (
|
INSERT INTO team_members (
|
||||||
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted
|
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted, payouts_split
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, $8
|
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as TeamMemberId,
|
self.id as TeamMemberId,
|
||||||
@@ -426,6 +426,7 @@ impl TeamMember {
|
|||||||
self.organization_permissions.map(|p| p.bits() as i64),
|
self.organization_permissions.map(|p| p.bits() as i64),
|
||||||
self.is_owner,
|
self.is_owner,
|
||||||
self.accepted,
|
self.accepted,
|
||||||
|
self.payouts_split
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -126,70 +126,42 @@ pub struct VersionFileBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl VersionFileBuilder {
|
impl VersionFileBuilder {
|
||||||
pub async fn insert_many(
|
pub async fn insert(
|
||||||
version_files: Vec<Self>,
|
self,
|
||||||
version_id: VersionId,
|
version_id: VersionId,
|
||||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
) -> Result<FileId, DatabaseError> {
|
) -> Result<FileId, DatabaseError> {
|
||||||
let file_id = generate_file_id(transaction).await?;
|
let file_id = generate_file_id(&mut *transaction).await?;
|
||||||
|
|
||||||
let (file_ids, version_ids, urls, filenames, primary, sizes, file_types): (
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
) = version_files
|
|
||||||
.iter()
|
|
||||||
.map(|f| {
|
|
||||||
(
|
|
||||||
file_id.0,
|
|
||||||
version_id.0,
|
|
||||||
f.url.clone(),
|
|
||||||
f.filename.clone(),
|
|
||||||
f.primary,
|
|
||||||
f.size as i32,
|
|
||||||
f.file_type.map(|x| x.to_string()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.multiunzip();
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)
|
INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)
|
||||||
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::varchar[], $4::varchar[], $5::bool[], $6::integer[], $7::varchar[])
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
",
|
",
|
||||||
&file_ids[..],
|
file_id as FileId,
|
||||||
&version_ids[..],
|
version_id as VersionId,
|
||||||
&urls[..],
|
self.url,
|
||||||
&filenames[..],
|
self.filename,
|
||||||
&primary[..],
|
self.primary,
|
||||||
&sizes[..],
|
self.size as i32,
|
||||||
&file_types[..] as &[Option<String>],
|
self.file_type.map(|x| x.as_str()),
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (file_ids, algorithms, hashes): (Vec<_>, Vec<_>, Vec<_>) = version_files
|
for hash in self.hashes {
|
||||||
.into_iter()
|
sqlx::query!(
|
||||||
.flat_map(|f| {
|
"
|
||||||
f.hashes
|
INSERT INTO hashes (file_id, algorithm, hash)
|
||||||
.into_iter()
|
VALUES ($1, $2, $3)
|
||||||
.map(|h| (file_id.0, h.algorithm, h.hash))
|
",
|
||||||
})
|
file_id as FileId,
|
||||||
.multiunzip();
|
hash.algorithm,
|
||||||
sqlx::query!(
|
hash.hash,
|
||||||
"
|
)
|
||||||
INSERT INTO hashes (file_id, algorithm, hash)
|
.execute(&mut **transaction)
|
||||||
SELECT * FROM UNNEST($1::bigint[], $2::varchar[], $3::bytea[])
|
.await?;
|
||||||
",
|
}
|
||||||
&file_ids[..],
|
|
||||||
&algorithms[..],
|
|
||||||
&hashes[..],
|
|
||||||
)
|
|
||||||
.execute(&mut **transaction)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(file_id)
|
Ok(file_id)
|
||||||
}
|
}
|
||||||
@@ -242,7 +214,10 @@ impl VersionBuilder {
|
|||||||
version_id,
|
version_id,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
VersionFileBuilder::insert_many(files, self.version_id, transaction).await?;
|
|
||||||
|
for file in files {
|
||||||
|
file.insert(version_id, transaction).await?;
|
||||||
|
}
|
||||||
|
|
||||||
DependencyBuilder::insert_many(dependencies, self.version_id, transaction).await?;
|
DependencyBuilder::insert_many(dependencies, self.version_id, transaction).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -83,24 +83,23 @@ impl LegacyProject {
|
|||||||
let mut game_versions = Vec::new();
|
let mut game_versions = Vec::new();
|
||||||
|
|
||||||
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
||||||
// We'll prioritize 'modpack' first, then 'mod', and if neither are found, use the first one.
|
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||||
// If there are no project types, default to 'project'
|
// If there are no project types, default to 'project'
|
||||||
let mut project_types = data.project_types;
|
let mut project_types = data.project_types;
|
||||||
if project_types.contains(&"modpack".to_string()) {
|
if project_types.contains(&"modpack".to_string()) {
|
||||||
project_types = vec!["modpack".to_string()];
|
project_types = vec!["modpack".to_string()];
|
||||||
} else if project_types.contains(&"mod".to_string()) {
|
|
||||||
project_types = vec!["mod".to_string()];
|
|
||||||
}
|
}
|
||||||
let project_type = project_types
|
|
||||||
|
let og_project_type = project_types
|
||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||||
|
|
||||||
let mut project_type = if project_type == "datapack" || project_type == "plugin" {
|
let mut project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||||
// These are not supported in V2, so we'll just use 'mod' instead
|
// These are not supported in V2, so we'll just use 'mod' instead
|
||||||
"mod".to_string()
|
"mod".to_string()
|
||||||
} else {
|
} else {
|
||||||
project_type
|
og_project_type.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut loaders = data.loaders;
|
let mut loaders = data.loaders;
|
||||||
@@ -120,7 +119,8 @@ impl LegacyProject {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
|
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
(client_side, server_side) = v2_reroute::convert_side_types_v2(&fields);
|
(client_side, server_side) =
|
||||||
|
v2_reroute::convert_side_types_v2(&fields, Some(&*og_project_type));
|
||||||
|
|
||||||
// - if loader is mrpack, this is a modpack
|
// - if loader is mrpack, this is a modpack
|
||||||
// the loaders are whatever the corresponding loader fields are
|
// the loaders are whatever the corresponding loader fields are
|
||||||
|
|||||||
@@ -75,24 +75,22 @@ impl LegacyResultSearchProject {
|
|||||||
display_categories.dedup();
|
display_categories.dedup();
|
||||||
|
|
||||||
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
// V2 versions only have one project type- v3 versions can rarely have multiple.
|
||||||
// We'll prioritize 'modpack' first, then 'mod', and if neither are found, use the first one.
|
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
|
||||||
// If there are no project types, default to 'project'
|
// If there are no project types, default to 'project'
|
||||||
let mut project_types = result_search_project.project_types;
|
let mut project_types = result_search_project.project_types;
|
||||||
if project_types.contains(&"modpack".to_string()) {
|
if project_types.contains(&"modpack".to_string()) {
|
||||||
project_types = vec!["modpack".to_string()];
|
project_types = vec!["modpack".to_string()];
|
||||||
} else if project_types.contains(&"mod".to_string()) {
|
|
||||||
project_types = vec!["mod".to_string()];
|
|
||||||
}
|
}
|
||||||
let project_type = project_types
|
let og_project_type = project_types
|
||||||
.first()
|
.first()
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
.unwrap_or("project".to_string()); // Default to 'project' if none are found
|
||||||
|
|
||||||
let project_type = if project_type == "datapack" || project_type == "plugin" {
|
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
|
||||||
// These are not supported in V2, so we'll just use 'mod' instead
|
// These are not supported in V2, so we'll just use 'mod' instead
|
||||||
"mod".to_string()
|
"mod".to_string()
|
||||||
} else {
|
} else {
|
||||||
project_type
|
og_project_type.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let loader_fields = result_search_project.loader_fields.clone();
|
let loader_fields = result_search_project.loader_fields.clone();
|
||||||
@@ -115,6 +113,7 @@ impl LegacyResultSearchProject {
|
|||||||
client_only,
|
client_only,
|
||||||
server_only,
|
server_only,
|
||||||
client_and_server,
|
client_and_server,
|
||||||
|
Some(&*og_project_type),
|
||||||
);
|
);
|
||||||
let client_side = client_side.to_string();
|
let client_side = client_side.to_string();
|
||||||
let server_side = server_side.to_string();
|
let server_side = server_side.to_string();
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ pub struct OrganizationId(pub u64);
|
|||||||
pub struct Organization {
|
pub struct Organization {
|
||||||
/// The id of the organization
|
/// The id of the organization
|
||||||
pub id: OrganizationId,
|
pub id: OrganizationId,
|
||||||
|
/// The slug of the organization
|
||||||
|
pub slug: String,
|
||||||
/// The title (and slug) of the organization
|
/// The title (and slug) of the organization
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The associated team of the organization
|
/// The associated team of the organization
|
||||||
@@ -38,6 +40,7 @@ impl Organization {
|
|||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: data.id.into(),
|
id: data.id.into(),
|
||||||
|
slug: data.slug,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
team_id: data.team_id.into(),
|
team_id: data.team_id.into(),
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
|||||||
@@ -497,7 +497,7 @@ pub async fn project_edit(
|
|||||||
let version = Version::from(version);
|
let version = Version::from(version);
|
||||||
let mut fields = version.fields;
|
let mut fields = version.fields;
|
||||||
let (current_client_side, current_server_side) =
|
let (current_client_side, current_server_side) =
|
||||||
v2_reroute::convert_side_types_v2(&fields);
|
v2_reroute::convert_side_types_v2(&fields, None);
|
||||||
let client_side = client_side.unwrap_or(current_client_side);
|
let client_side = client_side.unwrap_or(current_client_side);
|
||||||
let server_side = server_side.unwrap_or(current_server_side);
|
let server_side = server_side.unwrap_or(current_server_side);
|
||||||
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));
|
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));
|
||||||
|
|||||||
@@ -79,16 +79,22 @@ pub async fn loader_list(
|
|||||||
Ok(loaders) => {
|
Ok(loaders) => {
|
||||||
let loaders = loaders
|
let loaders = loaders
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|l| LoaderData {
|
.filter(|l| &*l.name != "mrpack")
|
||||||
icon: l.icon,
|
.map(|l| {
|
||||||
name: l.name,
|
let mut supported_project_types = l.supported_project_types;
|
||||||
// Add generic 'project' type to all loaders, which is the v2 representation of
|
// Add generic 'project' type to all loaders, which is the v2 representation of
|
||||||
// a project type before any versions are set.
|
// a project type before any versions are set.
|
||||||
supported_project_types: l
|
supported_project_types.push("project".to_string());
|
||||||
.supported_project_types
|
|
||||||
.into_iter()
|
if ["forge", "fabric", "quilt", "neoforge"].contains(&&*l.name) {
|
||||||
.chain(std::iter::once("project".to_string()))
|
supported_project_types.push("modpack".to_string());
|
||||||
.collect(),
|
}
|
||||||
|
|
||||||
|
LoaderData {
|
||||||
|
icon: l.icon,
|
||||||
|
name: l.name,
|
||||||
|
supported_project_types,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok(HttpResponse::Ok().json(loaders))
|
Ok(HttpResponse::Ok().json(loaders))
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ pub fn convert_side_type_facets_v3(facets: Vec<Vec<Vec<String>>>) -> Vec<Vec<Vec
|
|||||||
// this is not lossless. (See tests)
|
// this is not lossless. (See tests)
|
||||||
pub fn convert_side_types_v2(
|
pub fn convert_side_types_v2(
|
||||||
side_types: &HashMap<String, Value>,
|
side_types: &HashMap<String, Value>,
|
||||||
|
project_type: Option<&str>,
|
||||||
) -> (LegacySideType, LegacySideType) {
|
) -> (LegacySideType, LegacySideType) {
|
||||||
let client_and_server = side_types
|
let client_and_server = side_types
|
||||||
.get("client_and_server")
|
.get("client_and_server")
|
||||||
@@ -265,6 +266,7 @@ pub fn convert_side_types_v2(
|
|||||||
client_only,
|
client_only,
|
||||||
server_only,
|
server_only,
|
||||||
Some(client_and_server),
|
Some(client_and_server),
|
||||||
|
project_type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,29 +276,38 @@ pub fn convert_side_types_v2_bools(
|
|||||||
client_only: bool,
|
client_only: bool,
|
||||||
server_only: bool,
|
server_only: bool,
|
||||||
client_and_server: Option<bool>,
|
client_and_server: Option<bool>,
|
||||||
|
project_type: Option<&str>,
|
||||||
) -> (LegacySideType, LegacySideType) {
|
) -> (LegacySideType, LegacySideType) {
|
||||||
use LegacySideType::{Optional, Required, Unsupported};
|
use LegacySideType::{Optional, Required, Unknown, Unsupported};
|
||||||
|
|
||||||
let singleplayer = singleplayer.or(client_and_server).unwrap_or(false);
|
match project_type {
|
||||||
|
Some("plugin") => (Unsupported, Required),
|
||||||
|
Some("datapack") => (Optional, Required),
|
||||||
|
Some("shader") => (Required, Unsupported),
|
||||||
|
Some("resourcepack") => (Required, Unsupported),
|
||||||
|
_ => {
|
||||||
|
let singleplayer = singleplayer.or(client_and_server).unwrap_or(false);
|
||||||
|
|
||||||
match (singleplayer, client_only, server_only) {
|
match (singleplayer, client_only, server_only) {
|
||||||
// Only singleplayer
|
// Only singleplayer
|
||||||
(true, false, false) => (Required, Required),
|
(true, false, false) => (Required, Required),
|
||||||
|
|
||||||
// Client only and not server only
|
// Client only and not server only
|
||||||
(false, true, false) => (Required, Unsupported),
|
(false, true, false) => (Required, Unsupported),
|
||||||
(true, true, false) => (Required, Unsupported),
|
(true, true, false) => (Required, Unsupported),
|
||||||
|
|
||||||
// Server only and not client only
|
// Server only and not client only
|
||||||
(false, false, true) => (Unsupported, Required),
|
(false, false, true) => (Unsupported, Required),
|
||||||
(true, false, true) => (Unsupported, Required),
|
(true, false, true) => (Unsupported, Required),
|
||||||
|
|
||||||
// Both server only and client only
|
// Both server only and client only
|
||||||
(true, true, true) => (Optional, Optional),
|
(true, true, true) => (Optional, Optional),
|
||||||
(false, true, true) => (Optional, Optional),
|
(false, true, true) => (Optional, Optional),
|
||||||
|
|
||||||
// Bad type
|
// Bad type
|
||||||
(false, false, false) => (Unsupported, Unsupported),
|
(false, false, false) => (Unknown, Unknown),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +332,7 @@ mod tests {
|
|||||||
(Unsupported, Optional),
|
(Unsupported, Optional),
|
||||||
(Required, Optional),
|
(Required, Optional),
|
||||||
(Optional, Required),
|
(Optional, Required),
|
||||||
|
(Unsupported, Unsupported),
|
||||||
];
|
];
|
||||||
|
|
||||||
for client_side in [Required, Optional, Unsupported] {
|
for client_side in [Required, Optional, Unsupported] {
|
||||||
@@ -329,7 +341,7 @@ mod tests {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let side_types = convert_side_types_v3(client_side, server_side);
|
let side_types = convert_side_types_v3(client_side, server_side);
|
||||||
let (client_side2, server_side2) = convert_side_types_v2(&side_types);
|
let (client_side2, server_side2) = convert_side_types_v2(&side_types, None);
|
||||||
assert_eq!(client_side, client_side2);
|
assert_eq!(client_side, client_side2);
|
||||||
assert_eq!(server_side, server_side2);
|
assert_eq!(server_side, server_side2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ pub async fn organization_projects_get(
|
|||||||
"
|
"
|
||||||
SELECT m.id FROM organizations o
|
SELECT m.id FROM organizations o
|
||||||
INNER JOIN mods m ON m.organization_id = o.id
|
INNER JOIN mods m ON m.organization_id = o.id
|
||||||
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.name = $2 AND $2 IS NOT NULL)
|
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.slug = $2 AND $2 IS NOT NULL)
|
||||||
",
|
",
|
||||||
possible_organization_id.map(|x| x as i64),
|
possible_organization_id.map(|x| x as i64),
|
||||||
info
|
info
|
||||||
@@ -95,7 +95,9 @@ pub struct NewOrganization {
|
|||||||
length(min = 3, max = 64),
|
length(min = 3, max = 64),
|
||||||
regex = "crate::util::validate::RE_URL_SAFE"
|
regex = "crate::util::validate::RE_URL_SAFE"
|
||||||
)]
|
)]
|
||||||
// Title of the organization, also used as slug
|
pub slug: String,
|
||||||
|
// Title of the organization
|
||||||
|
#[validate(length(min = 3, max = 64))]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[validate(length(min = 3, max = 256))]
|
#[validate(length(min = 3, max = 256))]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
@@ -126,12 +128,12 @@ pub async fn organization_create(
|
|||||||
|
|
||||||
// Try title
|
// Try title
|
||||||
let name_organization_id_option: Option<OrganizationId> =
|
let name_organization_id_option: Option<OrganizationId> =
|
||||||
serde_json::from_str(&format!("\"{}\"", new_organization.name)).ok();
|
serde_json::from_str(&format!("\"{}\"", new_organization.slug)).ok();
|
||||||
let mut organization_strings = vec![];
|
let mut organization_strings = vec![];
|
||||||
if let Some(name_organization_id) = name_organization_id_option {
|
if let Some(name_organization_id) = name_organization_id_option {
|
||||||
organization_strings.push(name_organization_id.to_string());
|
organization_strings.push(name_organization_id.to_string());
|
||||||
}
|
}
|
||||||
organization_strings.push(new_organization.name.clone());
|
organization_strings.push(new_organization.slug.clone());
|
||||||
let results = Organization::get_many(&organization_strings, &mut *transaction, &redis).await?;
|
let results = Organization::get_many(&organization_strings, &mut *transaction, &redis).await?;
|
||||||
if !results.is_empty() {
|
if !results.is_empty() {
|
||||||
return Err(CreateError::SlugCollision);
|
return Err(CreateError::SlugCollision);
|
||||||
@@ -157,6 +159,7 @@ pub async fn organization_create(
|
|||||||
// Create organization
|
// Create organization
|
||||||
let organization = Organization {
|
let organization = Organization {
|
||||||
id: organization_id,
|
id: organization_id,
|
||||||
|
slug: new_organization.slug.clone(),
|
||||||
name: new_organization.name.clone(),
|
name: new_organization.name.clone(),
|
||||||
description: new_organization.description.clone(),
|
description: new_organization.description.clone(),
|
||||||
team_id,
|
team_id,
|
||||||
@@ -336,7 +339,8 @@ pub struct OrganizationEdit {
|
|||||||
length(min = 3, max = 64),
|
length(min = 3, max = 64),
|
||||||
regex = "crate::util::validate::RE_URL_SAFE"
|
regex = "crate::util::validate::RE_URL_SAFE"
|
||||||
)]
|
)]
|
||||||
// Title of the organization, also used as slug
|
pub slug: Option<String>,
|
||||||
|
#[validate(length(min = 3, max = 64))]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,8 +410,28 @@ pub async fn organizations_edit(
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE organizations
|
||||||
|
SET name = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
name,
|
||||||
|
id as database::models::ids::OrganizationId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let name_organization_id_option: Option<u64> = parse_base62(name).ok();
|
if let Some(slug) = &new_organization.slug {
|
||||||
|
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||||
|
return Err(ApiError::CustomAuthentication(
|
||||||
|
"You do not have the permissions to edit the slug of this organization!"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_organization_id_option: Option<u64> = parse_base62(slug).ok();
|
||||||
if let Some(name_organization_id) = name_organization_id_option {
|
if let Some(name_organization_id) = name_organization_id_option {
|
||||||
let results = sqlx::query!(
|
let results = sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -420,26 +444,26 @@ pub async fn organizations_edit(
|
|||||||
|
|
||||||
if results.exists.unwrap_or(true) {
|
if results.exists.unwrap_or(true) {
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"name collides with other organization's id!".to_string(),
|
"slug collides with other organization's id!".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the new name is different from the old one
|
// Make sure the new name is different from the old one
|
||||||
// We are able to unwrap here because the name is always set
|
// We are able to unwrap here because the name is always set
|
||||||
if !name.eq(&organization_item.name.clone()) {
|
if !slug.eq(&organization_item.slug.clone()) {
|
||||||
let results = sqlx::query!(
|
let results = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT EXISTS(SELECT 1 FROM organizations WHERE name = LOWER($1))
|
SELECT EXISTS(SELECT 1 FROM organizations WHERE LOWER(slug) = LOWER($1))
|
||||||
",
|
",
|
||||||
name
|
slug
|
||||||
)
|
)
|
||||||
.fetch_one(&mut *transaction)
|
.fetch_one(&mut *transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if results.exists.unwrap_or(true) {
|
if results.exists.unwrap_or(true) {
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"Name collides with other organization's id!".to_string(),
|
"slug collides with other organization's id!".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,10 +471,10 @@ pub async fn organizations_edit(
|
|||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
UPDATE organizations
|
UPDATE organizations
|
||||||
SET name = LOWER($1)
|
SET slug = $1
|
||||||
WHERE (id = $2)
|
WHERE (id = $2)
|
||||||
",
|
",
|
||||||
Some(name),
|
Some(slug),
|
||||||
id as database::models::ids::OrganizationId,
|
id as database::models::ids::OrganizationId,
|
||||||
)
|
)
|
||||||
.execute(&mut *transaction)
|
.execute(&mut *transaction)
|
||||||
@@ -460,7 +484,7 @@ pub async fn organizations_edit(
|
|||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
database::models::Organization::clear_cache(
|
database::models::Organization::clear_cache(
|
||||||
organization_item.id,
|
organization_item.id,
|
||||||
Some(organization_item.name),
|
Some(organization_item.slug),
|
||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -578,7 +602,7 @@ pub async fn organization_delete(
|
|||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
database::models::Organization::clear_cache(organization.id, Some(organization.name), &redis)
|
database::models::Organization::clear_cache(organization.id, Some(organization.slug), &redis)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for team_id in organization_project_teams {
|
for team_id in organization_project_teams {
|
||||||
@@ -994,7 +1018,7 @@ pub async fn organization_icon_edit(
|
|||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
database::models::Organization::clear_cache(
|
database::models::Organization::clear_cache(
|
||||||
organization_item.id,
|
organization_item.id,
|
||||||
Some(organization_item.name),
|
Some(organization_item.slug),
|
||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1079,7 +1103,7 @@ pub async fn delete_organization_icon(
|
|||||||
|
|
||||||
database::models::Organization::clear_cache(
|
database::models::Organization::clear_cache(
|
||||||
organization_item.id,
|
organization_item.id,
|
||||||
Some(organization_item.name),
|
Some(organization_item.slug),
|
||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub enum CreateError {
|
|||||||
InvalidCategory(String),
|
InvalidCategory(String),
|
||||||
#[error("Invalid file type for version file: {0}")]
|
#[error("Invalid file type for version file: {0}")]
|
||||||
InvalidFileType(String),
|
InvalidFileType(String),
|
||||||
#[error("Slug collides with other project's id!")]
|
#[error("Slug is already taken!")]
|
||||||
SlugCollision,
|
SlugCollision,
|
||||||
#[error("Authentication Error: {0}")]
|
#[error("Authentication Error: {0}")]
|
||||||
Unauthorized(#[from] AuthenticationError),
|
Unauthorized(#[from] AuthenticationError),
|
||||||
@@ -612,22 +612,22 @@ async fn project_create_inner(
|
|||||||
additional_categories.extend(ids.values());
|
additional_categories.extend(ids.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should only be owner if not attached to an organization
|
let mut members = vec![];
|
||||||
let is_owner = project_create_data.organization_id.is_none();
|
|
||||||
|
|
||||||
let team = models::team_item::TeamBuilder {
|
if project_create_data.organization_id.is_none() {
|
||||||
members: vec![models::team_item::TeamMemberBuilder {
|
members.push(models::team_item::TeamMemberBuilder {
|
||||||
user_id: current_user.id.into(),
|
user_id: current_user.id.into(),
|
||||||
role: crate::models::teams::OWNER_ROLE.to_owned(),
|
role: crate::models::teams::OWNER_ROLE.to_owned(),
|
||||||
is_owner,
|
is_owner: true,
|
||||||
// Allow all permissions for project creator, even if attached to a project
|
|
||||||
permissions: ProjectPermissions::all(),
|
permissions: ProjectPermissions::all(),
|
||||||
organization_permissions: None,
|
organization_permissions: None,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
payouts_split: Decimal::ONE_HUNDRED,
|
payouts_split: Decimal::ONE_HUNDRED,
|
||||||
ordering: 0,
|
ordering: 0,
|
||||||
}],
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
|
let team = models::team_item::TeamBuilder { members };
|
||||||
|
|
||||||
let team_id = team.insert(&mut *transaction).await?;
|
let team_id = team.insert(&mut *transaction).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ pub async fn team_members_get_project(
|
|||||||
}
|
}
|
||||||
let members_data =
|
let members_data =
|
||||||
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
|
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
|
||||||
let users = crate::database::models::User::get_many_ids(
|
let users = User::get_many_ids(
|
||||||
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -73,14 +73,14 @@ pub async fn team_members_get_project(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let user_id = current_user.as_ref().map(|x| x.id.into());
|
let user_id = current_user.as_ref().map(|x| x.id.into());
|
||||||
|
let logged_in = if let Some(user_id) = user_id {
|
||||||
|
let (team_member, organization_team_member) =
|
||||||
|
TeamMember::get_for_project_permissions(&project.inner, user_id, &**pool).await?;
|
||||||
|
|
||||||
let logged_in = current_user
|
team_member.is_some() || organization_team_member.is_some()
|
||||||
.and_then(|user| {
|
} else {
|
||||||
members_data
|
false
|
||||||
.iter()
|
};
|
||||||
.find(|x| x.user_id == user.id.into() && x.accepted)
|
|
||||||
})
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
let team_members: Vec<_> = members_data
|
let team_members: Vec<_> = members_data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -731,7 +731,9 @@ async fn upload_file_to_version_inner(
|
|||||||
"At least one file must be specified".to_string(),
|
"At least one file must be specified".to_string(),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
VersionFileBuilder::insert_many(file_builders, version_id, &mut *transaction).await?;
|
for file in file_builders {
|
||||||
|
file.insert(version_id, &mut *transaction).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear version cache
|
// Clear version cache
|
||||||
|
|||||||
@@ -163,6 +163,9 @@ pub async fn index_local(
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
categories.extend(mrpack_loaders);
|
categories.extend(mrpack_loaders);
|
||||||
|
if loader_fields.contains_key("mrpack_loaders") {
|
||||||
|
categories.retain(|x| *x != "mrpack");
|
||||||
|
}
|
||||||
|
|
||||||
let gallery = m
|
let gallery = m
|
||||||
.gallery_items
|
.gallery_items
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use serde_json::json;
|
|||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
// TODO: Revisit this with the new modify_json in the version maker
|
// TODO: Revisit this wit h the new modify_json in the version maker
|
||||||
// That change here should be able to simplify it vastly
|
// That change here should be able to simplify it vastly
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
@@ -61,17 +61,13 @@ async fn search_projects() {
|
|||||||
),
|
),
|
||||||
// Project type change
|
// Project type change
|
||||||
// Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack'
|
// Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack'
|
||||||
(json!([["categories:mrpack"]]), vec![4]),
|
// (json!([["categories:mrpack"]]), vec![4]),
|
||||||
|
// (
|
||||||
|
// json!([["categories:fabric"]]),
|
||||||
|
// vec![4],
|
||||||
|
// ),
|
||||||
(
|
(
|
||||||
json!([["categories:mrpack"], ["categories:fabric"]]),
|
json!([["categories:fabric"], ["project_types:modpack"]]),
|
||||||
vec![4],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
json!([
|
|
||||||
["categories:mrpack"],
|
|
||||||
["categories:fabric"],
|
|
||||||
["project_types:modpack"]
|
|
||||||
]),
|
|
||||||
vec![4],
|
vec![4],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -259,14 +259,14 @@ async fn search_projects() {
|
|||||||
),
|
),
|
||||||
// Project type change
|
// Project type change
|
||||||
// Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack'
|
// Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack'
|
||||||
(json!([["categories:mrpack"]]), vec![4]),
|
// (json!([["categories:mrpack"]]), vec![4]),
|
||||||
(
|
// (
|
||||||
json!([["categories:mrpack"], ["categories:fabric"]]),
|
// json!([["categories:mrpack"], ["categories:fabric"]]),
|
||||||
vec![4],
|
// vec![4],
|
||||||
),
|
// ),
|
||||||
(
|
(
|
||||||
json!([
|
json!([
|
||||||
["categories:mrpack"],
|
// ["categories:mrpack"],
|
||||||
["categories:fabric"],
|
["categories:fabric"],
|
||||||
["project_type:modpack"]
|
["project_type:modpack"]
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ async fn get_tags() {
|
|||||||
let loader_names = loaders.into_iter().map(|x| x.name).collect::<HashSet<_>>();
|
let loader_names = loaders.into_iter().map(|x| x.name).collect::<HashSet<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loader_names,
|
loader_names,
|
||||||
["fabric", "forge", "mrpack", "bukkit", "waterfall"]
|
["fabric", "forge", "bukkit", "waterfall"]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
Reference in New Issue
Block a user