Misc v3 linear tasks (#767)

* v3_reroute 404 error

* hash change

* fixed issue with error conversion

* added new model confirmation tests
+ title name change

* renaming, fields

* owner; test changes

* clippy prepare

* fmt

* merge fixes

* clippy

* working merge

* revs

* merge fixes
This commit is contained in:
Wyatt Verchere 2023-12-01 19:15:00 -08:00 committed by GitHub
parent 2d92b08404
commit a70df067bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
119 changed files with 2897 additions and 1334 deletions

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE\n WHERE o.id = $1\n ",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id \n FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE\n WHERE v.id = $1\n ",
"describe": {
"columns": [
{
@ -25,28 +25,38 @@
},
{
"ordinal": 4,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "ordering",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "mod_id",
"type_info": "Int8"
}
],
"parameters": {
@ -61,11 +71,13 @@
false,
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "65b5acdce6675d9c2abe636793dafef8ec915ddcc11a2735c66a49a48f314dd6"
"hash": "06bf1b34b70f5e61bf619c4d7706d07d6db413751ecab86896a708c8539e38b6"
}

View File

@ -1,120 +0,0 @@
{
"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 '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": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "color",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "icon_url",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "avatar_url",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "categories",
"type_info": "VarcharArray"
},
{
"ordinal": 9,
"name": "loaders",
"type_info": "VarcharArray"
},
{
"ordinal": 10,
"name": "project_types",
"type_info": "VarcharArray"
},
{
"ordinal": 11,
"name": "games",
"type_info": "VarcharArray"
},
{
"ordinal": 12,
"name": "gallery",
"type_info": "VarcharArray"
},
{
"ordinal": 13,
"name": "featured_gallery",
"type_info": "VarcharArray"
},
{
"ordinal": 14,
"name": "version_fields",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "loader_fields",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"name": "loader_field_enum_values",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8",
"TextArray",
"Text"
]
},
"nullable": [
false,
false,
false,
true,
true,
true,
false,
true,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
"hash": "108fe88fbd116817e48719c370a63b7d0d56b6de443db0e4466237ee3e93fbde"
}

View File

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_links\n WHERE joining_mod_id = $1 AND joining_platform_id IN (\n SELECT id FROM link_platforms WHERE name = ANY($2)\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"TextArray"
]
},
"nullable": []
},
"hash": "186d0e933ece20163915926293a01754ff571de4f06e521bb4f7c0207268e03b"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT o.id, o.title, 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.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": {
"columns": [
{
@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -48,5 +48,5 @@
true
]
},
"hash": "9608a95084c55d939d3f908f3dd7e53cb1c9455b5d53868993147bf6abc42ffb"
"hash": "26a6271a6d365e64c68ea5855109f1597a121b2e0075b20e2bc34659a269294b"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id \n FROM versions v\n INNER JOIN mods m ON m.id = v.mod_id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE\n WHERE v.id = $1\n ",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM organizations o\n INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE\n WHERE o.id = $1\n ",
"describe": {
"columns": [
{
@ -25,32 +25,32 @@
},
{
"ordinal": 4,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 8,
"name": "ordering",
"type_info": "Int8"
},
{
"ordinal": 9,
"name": "mod_id",
"name": "ordering",
"type_info": "Int8"
}
],
@ -66,12 +66,12 @@
false,
false,
false,
true,
false,
true,
false,
false,
false
]
},
"hash": "8c93ad7aa81a0502494ff98dd6120c34d583d1a205b4c97ac54a7230b8c23765"
"hash": "2e3ce3eafee2cd110085a94b122884fe25591aa6f48256abbb6c8d973efe932e"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods_gallery\n SET title = $2\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "3c061c1888cb14655288cdbb2dad22f6cb51d6be3736e8d8206f918a9a64aec7"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE collections\n SET title = $1\n WHERE (id = $2)\n ",
"query": "\n UPDATE collections\n SET name = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "ce2e7642142f79bdce78ba3316fe402e18ae203cc65fe79f724d37a7076df2dd"
"hash": "4b089a5d9408febe64af1cf5f78cc11c33f6702637c03c1ed9d24df8a847f91a"
}

View File

@ -0,0 +1,119 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT m.id id, m.name name, 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.is_owner = TRUE 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": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "color",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "icon_url",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "slug",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "avatar_url",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "categories",
"type_info": "VarcharArray"
},
{
"ordinal": 9,
"name": "loaders",
"type_info": "VarcharArray"
},
{
"ordinal": 10,
"name": "project_types",
"type_info": "VarcharArray"
},
{
"ordinal": 11,
"name": "games",
"type_info": "VarcharArray"
},
{
"ordinal": 12,
"name": "gallery",
"type_info": "VarcharArray"
},
{
"ordinal": 13,
"name": "featured_gallery",
"type_info": "VarcharArray"
},
{
"ordinal": 14,
"name": "version_fields",
"type_info": "Jsonb"
},
{
"ordinal": 15,
"name": "loader_fields",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"name": "loader_field_enum_values",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8",
"TextArray"
]
},
"nullable": [
false,
false,
false,
true,
true,
true,
false,
true,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
"hash": "4b9e5d78245ac083c167be708c196170c543a2157dbfa9d6249d98dc13bfaf72"
}

View File

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM mods_links\n WHERE joining_mod_id = $1 AND joining_platform_id IN (\n SELECT id FROM link_platforms WHERE name = ANY($2)\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"TextArray"
]
},
"nullable": []
},
"hash": "4d0e2d4345aeab5ee7eed847c03c913073eeb43caaf299cddcac6e41351661fd"
}

View File

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods_gallery\n SET name = $2\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "568ca221aaacb7222bf5099f59ae6bc3d96fbffaf91394115c29029ae9ea4108"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO collections (\n id, user_id, title, description, \n created, icon_url, status\n )\n VALUES (\n $1, $2, $3, $4, \n $5, $6, $7\n )\n ",
"query": "\n INSERT INTO collections (\n id, user_id, name, description, \n created, icon_url, status\n )\n VALUES (\n $1, $2, $3, $4, \n $5, $6, $7\n )\n ",
"describe": {
"columns": [],
"parameters": {
@ -16,5 +16,5 @@
},
"nullable": []
},
"hash": "536f628092168eead27519db013ec8a1510a06f27e699839bac9dc85d16d99c2"
"hash": "5ba9860050d19de8fe81482cbbdc68b32092609cc7150c7fcf491f342c5d9770"
}

View File

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "5e7146bc9dc9145cf3d01875ee599ada89e28c63fd10b3f23680d6660d0e57a2"
}

View File

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "6366891bb34a14278f1ae857b8d6f68dff44badae9ae5c5aceba3c32e8d00356"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ",
"query": "\n SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering\n FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE\n WHERE m.id = $1\n ",
"describe": {
"columns": [
{
@ -25,26 +25,31 @@
},
{
"ordinal": 4,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "ordering",
"type_info": "Int8"
}
@ -61,11 +66,12 @@
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "0244926b35b964da2b50ccf82aff001250a3751d2314707c4884066432aa4753"
"hash": "740c4343d7357af6820e28a3e1f165cbbc3f967c4dfbeeb13a0c63f78e072895"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n ",
"query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.is_owner = TRUE\n ",
"describe": {
"columns": [
{
@ -11,13 +11,12 @@
],
"parameters": {
"Left": [
"Int8",
"Text"
"Int8"
]
},
"nullable": [
false
]
},
"hash": "2f4a620f954c7488e8bdb94a3d6968cec6d1332942b9e9f60925d14a8c2040f7"
"hash": "834be4337c2dcc2a5f38c0f4ae0a2065b5a30fc43bb32ccfe8d58e9f3da24937"
}

View File

@ -1,28 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\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 ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "83dc16cc7a0f4507e308a06f4924065e4ea25de0210be222ceae8eb645f888e3"
}

View File

@ -1,6 +1,6 @@
{
"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\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 ",
"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.name name, 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.is_owner = TRUE 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": [
{
@ -15,7 +15,7 @@
},
{
"ordinal": 2,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -137,8 +137,7 @@
"parameters": {
"Left": [
"TextArray",
"TextArray",
"Text"
"TextArray"
]
},
"nullable": [
@ -170,5 +169,5 @@
null
]
},
"hash": "c81df8078d6cba7f43a3d47a0b9cd365c7321c9ed304d6dd137cb2198780ff3a"
"hash": "94de8109ff9f95be5e9f70c629fa9b1cfb2e9a1c094bc5e0d529a314a77fb4d7"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, team_id, role AS member_role, permissions, organization_permissions,\n accepted, payouts_split, role,\n ordering, user_id\n \n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n ORDER BY ordering\n ",
"query": "\n SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,\n accepted, payouts_split, role,\n ordering, user_id\n \n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n ORDER BY ordering\n ",
"describe": {
"columns": [
{
@ -20,36 +20,41 @@
},
{
"ordinal": 3,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "ordering",
"type_info": "Int8"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "user_id",
"type_info": "Int8"
}
@ -65,6 +70,7 @@
false,
false,
false,
false,
true,
false,
false,
@ -73,5 +79,5 @@
false
]
},
"hash": "ce20a9c53249e255be7312819f505d935d3ab2ee3c21a6422e5b12155c159bd7"
"hash": "9abdd9a2018e7bfe26836dd5463ba0923ef0a76c32ca258faf55fc3301c567bf"
}

View File

@ -0,0 +1,28 @@
{
"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 ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Varchar",
"Varchar",
"Varchar",
"Timestamptz",
"Int4",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Text",
"Int4",
"Varchar"
]
},
"nullable": []
},
"hash": "9b0c04bc7d44a60c175259cfe86b8e7ba0340ea9c89be30a89cc224d0f7e9727"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET body = $1\n WHERE (id = $2)\n ",
"query": "\n UPDATE mods\n SET name = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "dc6aa2e7bfd5d5004620ddd4cd6a47ecc56159e1489054e0652d56df802fb5e5"
"hash": "9c1b6ba7cbe2619ff767ee7bbfb01725dc3324d284b2f20cf393574ab3bc655f"
}

View File

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE team_members\n SET is_owner = $1\n WHERE (team_id = $2 AND user_id = $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bool",
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "a25b09712476fa4b12d98e08a4d260260e250e46fc68d806bf6372130cc65e1b"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n ",
"query": "\n UPDATE mods\n SET summary = $1\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "3d700aaeb0d5129ac8c297ee0542757435a50a35ec94582d9d6ce67aa5302291"
"hash": "b677e66031752e66d2219079a559e368c6cea1800da8a5f9d50ba5b1ac3a15fc"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO organizations (id, title, team_id, description, icon_url, color)\n VALUES ($1, $2, $3, $4, $5, $6)\n ",
"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": {
@ -15,5 +15,5 @@
},
"nullable": []
},
"hash": "77be410d0687b65b3554a35740fcf3c02418c5897856000716a35c02eed43d5a"
"hash": "bd48b18b9bef07185d2d050c7c978904cfbdf4ec765b7d3568f930939e236cbe"
}

View File

@ -1,16 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ",
"query": "\n DELETE FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND NOT is_owner = TRUE)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
"Int8"
]
},
"nullable": []
},
"hash": "8ba2b2c38958f1c542e514fc62ab4682f58b0b442ac1842d20625420698e34ec"
"hash": "c0bd8a50915398377b6e8a6c046a2d406c3d9e7721647c8a6f4fcf9e7c72bc25"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, team_id, role AS member_role, permissions, organization_permissions,\n accepted, payouts_split, \n ordering, user_id\n FROM team_members\n WHERE team_id = ANY($1)\n ORDER BY team_id, ordering;\n ",
"query": "\n SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,\n accepted, payouts_split, \n ordering, user_id\n FROM team_members\n WHERE team_id = ANY($1)\n ORDER BY team_id, ordering;\n ",
"describe": {
"columns": [
{
@ -20,31 +20,36 @@
},
{
"ordinal": 3,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "ordering",
"type_info": "Int8"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "user_id",
"type_info": "Int8"
}
@ -59,6 +64,7 @@
false,
false,
false,
false,
true,
false,
false,
@ -66,5 +72,5 @@
false
]
},
"hash": "e6db02891be261e61a25716b83c1298482eb9a04f0c026532030aeb374405f13"
"hash": "c387574b32f6b70adc88132df96fbbc7dd57a6f633a787dd31aafc0584547345"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.title) = ANY($2)\n GROUP BY o.id;\n ",
"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 ",
"describe": {
"columns": [
{
@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -49,5 +49,5 @@
true
]
},
"hash": "30307fb92fd2d8e1f03f21f8ad76f285ef8cb2bf8f40f9facafaae3f8c75d587"
"hash": "ca9f3298ff92051412f5096690b3314fe91fe0b7c79ab2f7d09396af47b85ee6"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO team_members (id, team_id, user_id, role, permissions, organization_permissions, accepted, payouts_split, ordering)\n SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::int8[], $6::int8[], $7::bool[], $8::numeric[], $9::int8[])\n ",
"query": "\n INSERT INTO team_members (id, team_id, user_id, role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering)\n SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::bool[], $6::int8[], $7::int8[], $8::bool[], $9::numeric[], $10::int8[])\n ",
"describe": {
"columns": [],
"parameters": {
@ -9,6 +9,7 @@
"Int8Array",
"Int8Array",
"VarcharArray",
"BoolArray",
"Int8Array",
"Int8Array",
"BoolArray",
@ -18,5 +19,5 @@
},
"nullable": []
},
"hash": "7075dc0343dab7c4dd4469b4af095232dcdd056a15d928a6d93556daf6fd327c"
"hash": "d137055262526c5e9295a712430c528b9d0f37aacbb53aeb530d3a64fc49365e"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, featured, title, description, ordering\n )\n SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::bool[], $4::varchar[], $5::varchar[], $6::bigint[])\n ",
"query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, featured, name, description, ordering\n )\n SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::bool[], $4::varchar[], $5::varchar[], $6::bigint[])\n ",
"describe": {
"columns": [],
"parameters": {
@ -15,5 +15,5 @@
},
"nullable": []
},
"hash": "5d65f89c020ae032f26d742c37afe47876911eb3a16a6852299b98f2a8251fb4"
"hash": "d5b2cfec04f4a74b5a3767047732e67c3107dcf4a386a4af552191460216f45d"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(SELECT 1 FROM organizations WHERE title = LOWER($1))\n ",
"query": "\n SELECT EXISTS(SELECT 1 FROM organizations WHERE name = LOWER($1))\n ",
"describe": {
"columns": [
{
@ -18,5 +18,5 @@
null
]
},
"hash": "e3389d233c75649e95456d504d1b716d520a03a8a3e0cc5311a4a753f1f04614"
"hash": "da30019590b9d0f7e21668997e780044c67a7c5d225e556c7ec2a4d7709db5ea"
}

View File

@ -1,6 +1,6 @@
{
"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.title = $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.name = $2 AND $2 IS NOT NULL)\n ",
"describe": {
"columns": [
{
@ -19,5 +19,5 @@
false
]
},
"hash": "009bce5eee6ed65d9dc0899a4e24da528507a3f00b7ec997fa9ccdd7599655b1"
"hash": "da962cbb02919ea79e1106e6e5de39224d240d9b8afb5cead28578ca65e281ae"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n ",
"query": "\n SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.user_id = $1\n GROUP BY n.id, n.user_id;\n ",
"describe": {
"columns": [
{
@ -15,7 +15,7 @@
},
{
"ordinal": 2,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -72,5 +72,5 @@
null
]
},
"hash": "c49cda8215982b699d7aee14614763c9b5b997489581293fc2ae3604697867fe"
"hash": "dc05295852b5a1d49be7906cd248566ffdfe790d7b61bd69969b00d558b41804"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, team_id, role AS member_role, permissions, organization_permissions,\n accepted, payouts_split, role,\n ordering, user_id\n FROM team_members\n WHERE (team_id = ANY($1) AND user_id = $2 AND accepted = TRUE)\n ORDER BY ordering\n ",
"query": "\n SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,\n accepted, payouts_split, role,\n ordering, user_id\n FROM team_members\n WHERE (team_id = ANY($1) AND user_id = $2 AND accepted = TRUE)\n ORDER BY ordering\n ",
"describe": {
"columns": [
{
@ -20,36 +20,41 @@
},
{
"ordinal": 3,
"name": "is_owner",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "permissions",
"type_info": "Int8"
},
{
"ordinal": 4,
"ordinal": 5,
"name": "organization_permissions",
"type_info": "Int8"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "accepted",
"type_info": "Bool"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "payouts_split",
"type_info": "Numeric"
},
{
"ordinal": 7,
"ordinal": 8,
"name": "role",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 9,
"name": "ordering",
"type_info": "Int8"
},
{
"ordinal": 9,
"ordinal": 10,
"name": "user_id",
"type_info": "Int8"
}
@ -65,6 +70,7 @@
false,
false,
false,
false,
true,
false,
false,
@ -73,5 +79,5 @@
false
]
},
"hash": "d55bdef50adf0b8a547022d0a041bec8618da02d82a1138da77d8885c0d9cfb9"
"hash": "ec8f310133cef187e8a6d101105210d6fcc194f67f671a8c4021ac23e0fb5dfc"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE organizations\n SET title = LOWER($1)\n WHERE (id = $2)\n ",
"query": "\n UPDATE organizations\n SET name = LOWER($1)\n WHERE (id = $2)\n ",
"describe": {
"columns": [],
"parameters": {
@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "07b692d2f89cdcc66da4e1a834f6fefe6a24c13c287490662585749b2b8baae3"
"hash": "eefe0f3e40273da9adea96cdef5fd5cff917a864a701408455cc6b02cd005cf7"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = ANY($1)\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ",
"query": "\n SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,\n JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = ANY($1)\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ",
"describe": {
"columns": [
{
@ -15,7 +15,7 @@
},
{
"ordinal": 2,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -72,5 +72,5 @@
null
]
},
"hash": "fce67ce3d0c27c64af85fb7d36661513bc5ea2e96fcf12f3a51c97999b01b83c"
"hash": "f0068d4e1303bfa69bf1c8d536e74395de5d6b6f7ba7389e8c934eeb8c10286f"
}

View File

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT c.id id, c.title title, c.description description,\n c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,\n c.updated updated, c.status status,\n ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods\n FROM collections c\n LEFT JOIN collections_mods cm ON cm.collection_id = c.id\n WHERE c.id = ANY($1)\n GROUP BY c.id;\n ",
"query": "\n SELECT c.id id, c.name name, c.description description,\n c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,\n c.updated updated, c.status status,\n ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods\n FROM collections c\n LEFT JOIN collections_mods cm ON cm.collection_id = c.id\n WHERE c.id = ANY($1)\n GROUP BY c.id;\n ",
"describe": {
"columns": [
{
@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "title",
"name": "name",
"type_info": "Varchar"
},
{
@ -72,5 +72,5 @@
null
]
},
"hash": "0b9f174d86badae0d30e34b32130c7cee69926e37db95494ab08f025d19cdb7c"
"hash": "f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0"
}

View File

@ -1,16 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND role = $3)\n ",
"query": "\n UPDATE team_members\n SET user_id = $1\n WHERE (user_id = $2 AND is_owner = TRUE)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text"
"Int8"
]
},
"nullable": []
},
"hash": "a1a8aa7cc5d7967fbc64b979489222d9f5c154e21227f0edcbce1d96dddad3c6"
"hash": "f73a0a6a79f97213477fc862101d0ced00500ab81336d129b5621581e9cd5e62"
}

View File

@ -0,0 +1,16 @@
-- rename 'title' to 'name' in all tables (collections, organizations, mods, mods_gallery, notifications, notifications_actions)
ALTER TABLE collections RENAME COLUMN title TO name;
ALTER TABLE organizations RENAME COLUMN title TO name;
ALTER TABLE mods RENAME COLUMN title TO name;
ALTER TABLE mods_gallery RENAME COLUMN title TO name;
ALTER TABLE notifications RENAME COLUMN title TO name;
ALTER TABLE notifications_actions RENAME COLUMN title TO name;
-- rename project 'description' to 'summary'
-- rename project 'body' to 'description'
ALTER TABLE mods RENAME COLUMN description TO summary;
ALTER TABLE mods RENAME COLUMN body TO description;
-- Adds 'is_owner' boolean to team members table- only one can be true.
ALTER TABLE team_members ADD COLUMN is_owner boolean NOT NULL DEFAULT false;
UPDATE team_members SET is_owner = true WHERE role = 'Owner';

View File

@ -12,7 +12,7 @@ const COLLECTIONS_NAMESPACE: &str = "collections";
pub struct CollectionBuilder {
pub collection_id: CollectionId,
pub user_id: UserId,
pub title: String,
pub name: String,
pub description: String,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
@ -25,7 +25,7 @@ impl CollectionBuilder {
) -> Result<CollectionId, DatabaseError> {
let collection_struct = Collection {
id: self.collection_id,
title: self.title,
name: self.name,
user_id: self.user_id,
description: self.description,
created: Utc::now(),
@ -44,7 +44,7 @@ impl CollectionBuilder {
pub struct Collection {
pub id: CollectionId,
pub user_id: UserId,
pub title: String,
pub name: String,
pub description: String,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
@ -62,7 +62,7 @@ impl Collection {
sqlx::query!(
"
INSERT INTO collections (
id, user_id, title, description,
id, user_id, name, description,
created, icon_url, status
)
VALUES (
@ -72,7 +72,7 @@ impl Collection {
",
self.id as CollectionId,
self.user_id as UserId,
&self.title,
&self.name,
&self.description,
self.created,
self.icon_url.as_ref(),
@ -190,7 +190,7 @@ impl Collection {
remaining_collections.iter().map(|x| x.0).collect();
let db_collections: Vec<Collection> = sqlx::query!(
"
SELECT c.id id, c.title title, c.description description,
SELECT c.id id, c.name name, c.description description,
c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,
c.updated updated, c.status status,
ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods
@ -209,7 +209,7 @@ impl Collection {
Collection {
id: CollectionId(id),
user_id: UserId(m.user_id),
title: m.title.clone(),
name: m.name.clone(),
description: m.description.clone(),
icon_url: m.icon_url.clone(),
color: m.color.map(|x| x as u32),

View File

@ -25,7 +25,7 @@ pub struct Notification {
pub struct NotificationAction {
pub id: NotificationActionId,
pub notification_id: NotificationId,
pub title: String,
pub name: String,
pub action_route_method: String,
pub action_route: String,
}
@ -122,8 +122,8 @@ impl Notification {
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
sqlx::query!(
"
SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
FROM notifications n
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
WHERE n.id = ANY($1)
@ -143,10 +143,10 @@ impl Notification {
read: row.read,
created: row.created,
body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| {
if let Some(title) = row.title {
if let Some(name) = row.name {
NotificationBody::LegacyMarkdown {
notification_type: row.notification_type,
title,
name,
text: row.text.unwrap_or_default(),
link: row.link.unwrap_or_default(),
actions: serde_json::from_value(
@ -186,8 +186,8 @@ impl Notification {
let db_notifications = sqlx::query!(
"
SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'title', na.title, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
FROM notifications n
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
WHERE n.user_id = $1
@ -206,10 +206,10 @@ impl Notification {
read: row.read,
created: row.created,
body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| {
if let Some(title) = row.title {
if let Some(name) = row.name {
NotificationBody::LegacyMarkdown {
notification_type: row.notification_type,
title,
name,
text: row.text.unwrap_or_default(),
link: row.link.unwrap_or_default(),
actions: serde_json::from_value(

View File

@ -16,7 +16,7 @@ pub struct Organization {
pub id: OrganizationId,
/// The title (and slug) of the organization
pub title: String,
pub name: String,
/// The associated team of the organization
pub team_id: TeamId,
@ -36,11 +36,11 @@ impl Organization {
) -> Result<(), super::DatabaseError> {
sqlx::query!(
"
INSERT INTO organizations (id, title, team_id, description, icon_url, color)
INSERT INTO organizations (id, name, team_id, description, icon_url, color)
VALUES ($1, $2, $3, $4, $5, $6)
",
self.id.0,
self.title,
self.name,
self.team_id as TeamId,
self.description,
self.icon_url,
@ -149,7 +149,7 @@ impl Organization {
{
remaining_strings.retain(|x| {
&to_base62(organization.id.0 as u64) != x
&& organization.title.to_lowercase() != x.to_lowercase()
&& organization.name.to_lowercase() != x.to_lowercase()
});
found_organizations.push(organization);
continue;
@ -166,9 +166,9 @@ impl Organization {
let organizations: Vec<Organization> = sqlx::query!(
"
SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
FROM organizations o
WHERE o.id = ANY($1) OR LOWER(o.title) = ANY($2)
WHERE o.id = ANY($1) OR LOWER(o.name) = ANY($2)
GROUP BY o.id;
",
&organization_ids_parsed,
@ -181,7 +181,7 @@ impl Organization {
.try_filter_map(|e| async {
Ok(e.right().map(|m| Organization {
id: OrganizationId(m.id),
title: m.title,
name: m.name,
team_id: TeamId(m.team_id),
description: m.description,
icon_url: m.icon_url,
@ -203,7 +203,7 @@ impl Organization {
redis
.set(
ORGANIZATIONS_TITLES_NAMESPACE,
&organization.title.to_lowercase(),
&organization.name.to_lowercase(),
&organization.id.0.to_string(),
None,
)
@ -226,7 +226,7 @@ impl Organization {
{
let result = sqlx::query!(
"
SELECT o.id, o.title, o.team_id, o.description, o.icon_url, o.color
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
FROM organizations o
LEFT JOIN mods m ON m.organization_id = o.id
WHERE m.id = $1
@ -240,7 +240,7 @@ impl Organization {
if let Some(result) = result {
Ok(Some(Organization {
id: OrganizationId(result.id),
title: result.title,
name: result.name,
team_id: TeamId(result.team_id),
description: result.description,
icon_url: result.icon_url,
@ -279,7 +279,7 @@ impl Organization {
super::project_item::Project::remove(project_id, transaction, redis).await?;
}
Organization::clear_cache(id, Some(organization.title), redis).await?;
Organization::clear_cache(id, Some(organization.name), redis).await?;
sqlx::query!(
"

View File

@ -54,7 +54,7 @@ impl LinkUrl {
pub struct GalleryItem {
pub image_url: String,
pub featured: bool,
pub title: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub ordering: i64,
@ -66,7 +66,7 @@ impl GalleryItem {
project_id: ProjectId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
let (project_ids, image_urls, featureds, titles, descriptions, orderings): (
let (project_ids, image_urls, featureds, names, descriptions, orderings): (
Vec<_>,
Vec<_>,
Vec<_>,
@ -80,7 +80,7 @@ impl GalleryItem {
project_id.0,
gi.image_url,
gi.featured,
gi.title,
gi.name,
gi.description,
gi.ordering,
)
@ -89,14 +89,14 @@ impl GalleryItem {
sqlx::query!(
"
INSERT INTO mods_gallery (
mod_id, image_url, featured, title, description, ordering
mod_id, image_url, featured, name, description, ordering
)
SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::bool[], $4::varchar[], $5::varchar[], $6::bigint[])
",
&project_ids[..],
&image_urls[..],
&featureds[..],
&titles[..] as &[Option<String>],
&names[..] as &[Option<String>],
&descriptions[..] as &[Option<String>],
&orderings[..]
)
@ -144,9 +144,9 @@ pub struct ProjectBuilder {
pub project_id: ProjectId,
pub team_id: TeamId,
pub organization_id: Option<OrganizationId>,
pub title: String,
pub name: String,
pub summary: String,
pub description: String,
pub body: String,
pub icon_url: Option<String>,
pub license_url: Option<String>,
pub categories: Vec<CategoryId>,
@ -171,10 +171,9 @@ impl ProjectBuilder {
id: self.project_id,
team_id: self.team_id,
organization_id: self.organization_id,
title: self.title,
name: self.name,
summary: self.summary,
description: self.description,
body: self.body,
body_url: None,
published: Utc::now(),
updated: Utc::now(),
approved: None,
@ -237,10 +236,9 @@ pub struct Project {
pub id: ProjectId,
pub team_id: TeamId,
pub organization_id: Option<OrganizationId>,
pub title: String,
pub name: String,
pub summary: String,
pub description: String,
pub body: String,
pub body_url: Option<String>,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub approved: Option<DateTime<Utc>>,
@ -269,7 +267,7 @@ impl Project {
sqlx::query!(
"
INSERT INTO mods (
id, team_id, title, description, body,
id, team_id, name, summary, description,
published, downloads, icon_url, status, requested_status,
license_url, license,
slug, color, monetization_status
@ -283,9 +281,9 @@ impl Project {
",
self.id as ProjectId,
self.team_id as TeamId,
&self.title,
&self.name,
&self.summary,
&self.description,
&self.body,
self.published,
self.downloads,
self.icon_url.as_ref(),
@ -623,7 +621,7 @@ impl Project {
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
'image_url', mg.image_url, 'featured', mg.featured, 'name', mg.name, 'description', mg.description, 'created', mg.created, 'ordering', mg.ordering
)
) filter (where image_url is not null) mods_gallery_json
FROM mods_gallery mg
@ -644,8 +642,8 @@ impl Project {
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,
SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,
m.icon_url icon_url, m.description description, m.published published,
m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,
m.license_url license_url,
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,
@ -693,10 +691,9 @@ impl Project {
id: ProjectId(id),
team_id: TeamId(m.team_id),
organization_id: m.organization_id.map(OrganizationId),
title: m.title.clone(),
description: m.description.clone(),
name: m.name.clone(),
summary: m.summary.clone(),
downloads: m.downloads,
body_url: None,
icon_url: m.icon_url.clone(),
published: m.published,
updated: m.updated,
@ -709,7 +706,7 @@ impl Project {
)),
license: m.license.clone(),
slug: m.slug.clone(),
body: m.body.clone(),
description: m.description.clone(),
follows: m.follows,
moderation_message: m.moderation_message,
moderation_message_body: m.moderation_message_body,

View File

@ -15,6 +15,7 @@ pub struct TeamBuilder {
pub struct TeamMemberBuilder {
pub user_id: UserId,
pub role: String,
pub is_owner: bool,
pub permissions: ProjectPermissions,
pub organization_permissions: Option<OrganizationPermissions>,
pub accepted: bool,
@ -50,6 +51,7 @@ impl TeamBuilder {
team_ids,
user_ids,
roles,
is_owners,
permissions,
organization_permissions,
accepteds,
@ -64,6 +66,7 @@ impl TeamBuilder {
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
) = members
.into_iter()
.map(|m| {
@ -71,6 +74,7 @@ impl TeamBuilder {
team.id.0,
m.user_id.0,
m.role,
m.is_owner,
m.permissions.bits() as i64,
m.organization_permissions.map(|p| p.bits() as i64),
m.accepted,
@ -81,13 +85,14 @@ impl TeamBuilder {
.multiunzip();
sqlx::query!(
"
INSERT INTO team_members (id, team_id, user_id, role, permissions, organization_permissions, accepted, payouts_split, ordering)
SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::int8[], $6::int8[], $7::bool[], $8::numeric[], $9::int8[])
INSERT INTO team_members (id, team_id, user_id, role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering)
SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::bool[], $6::int8[], $7::int8[], $8::bool[], $9::numeric[], $10::int8[])
",
&team_member_ids[..],
&team_ids[..],
&user_ids[..],
&roles[..],
&is_owners[..],
&permissions[..],
&organization_permissions[..] as &[Option<i64>],
&accepteds[..],
@ -162,6 +167,7 @@ pub struct TeamMember {
/// The ID of the user associated with the member
pub user_id: UserId,
pub role: String,
pub is_owner: bool,
// The permissions of the user in this project team
// For an organization team, these are the fallback permissions for any project in the organization
@ -233,7 +239,7 @@ impl TeamMember {
if !team_ids_parsed.is_empty() {
let teams: Vec<TeamMember> = sqlx::query!(
"
SELECT id, team_id, role AS member_role, permissions, organization_permissions,
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split,
ordering, user_id
FROM team_members
@ -248,6 +254,7 @@ impl TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
role: m.member_role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
@ -310,7 +317,7 @@ impl TeamMember {
let team_members = sqlx::query!(
"
SELECT id, team_id, role AS member_role, permissions, organization_permissions,
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split, role,
ordering, user_id
FROM team_members
@ -328,6 +335,7 @@ impl TeamMember {
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
@ -362,7 +370,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT id, team_id, role AS member_role, permissions, organization_permissions,
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split, role,
ordering, user_id
@ -382,6 +390,7 @@ impl TeamMember {
team_id: id,
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
@ -431,11 +440,10 @@ impl TeamMember {
sqlx::query!(
"
DELETE FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)
WHERE (team_id = $1 AND user_id = $2 AND NOT is_owner = TRUE)
",
id as TeamId,
user_id as UserId,
crate::models::teams::OWNER_ROLE,
)
.execute(&mut **transaction)
.await?;
@ -453,6 +461,7 @@ impl TeamMember {
new_accepted: Option<bool>,
new_payouts_split: Option<Decimal>,
new_ordering: Option<i64>,
new_is_owner: Option<bool>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
if let Some(permissions) = new_permissions {
@ -546,6 +555,21 @@ impl TeamMember {
.await?;
}
if let Some(is_owner) = new_is_owner {
sqlx::query!(
"
UPDATE team_members
SET is_owner = $1
WHERE (team_id = $2 AND user_id = $3)
",
is_owner,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
Ok(())
}
@ -559,7 +583,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE
WHERE m.id = $1
@ -576,6 +600,7 @@ impl TeamMember {
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
@ -600,7 +625,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
FROM organizations o
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE
WHERE o.id = $1
@ -617,6 +642,7 @@ impl TeamMember {
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
@ -641,7 +667,7 @@ impl TeamMember {
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id
FROM versions v
INNER JOIN mods m ON m.id = v.mod_id
INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE
@ -659,6 +685,7 @@ impl TeamMember {
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m

View File

@ -450,10 +450,9 @@ impl User {
"
SELECT m.id FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id
WHERE tm.user_id = $1 AND tm.role = $2
WHERE tm.user_id = $1 AND tm.is_owner = TRUE
",
id as UserId,
crate::models::teams::OWNER_ROLE
)
.fetch_many(&mut **transaction)
.try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) })
@ -470,11 +469,10 @@ impl User {
"
UPDATE team_members
SET user_id = $1
WHERE (user_id = $2 AND role = $3)
WHERE (user_id = $2 AND is_owner = TRUE)
",
deleted_user as UserId,
id as UserId,
crate::models::teams::OWNER_ROLE
)
.execute(&mut **transaction)
.await?;

View File

@ -209,7 +209,6 @@ impl VersionBuilder {
name: self.name,
version_number: self.version_number,
changelog: self.changelog,
changelog_url: None,
date_published: Utc::now(),
downloads: 0,
featured: self.featured,
@ -293,7 +292,6 @@ pub struct Version {
pub name: String,
pub version_number: String,
pub changelog: String,
pub changelog_url: Option<String>,
pub date_published: DateTime<Utc>,
pub downloads: i32,
pub version_type: String,
@ -644,7 +642,6 @@ impl Version {
name: v.version_name,
version_number: v.version_number,
changelog: v.changelog,
changelog_url: None,
date_published: v.date_published,
downloads: v.downloads,
version_type: v.version_type,
@ -1015,7 +1012,6 @@ mod tests {
name: Default::default(),
version_number: Default::default(),
changelog: Default::default(),
changelog_url: Default::default(),
downloads: Default::default(),
version_type: Default::default(),
featured: Default::default(),

View File

@ -1,3 +1,5 @@
// Legacy models from V2, where its useful to keep the struct for rerouting/conversion
pub mod notifications;
pub mod projects;
pub mod search;
pub mod teams;

View File

@ -0,0 +1,183 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::models::{
ids::{
NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId,
UserId, VersionId,
},
notifications::{Notification, NotificationAction, NotificationBody},
projects::ProjectStatus,
};
#[derive(Serialize, Deserialize)]
pub struct LegacyNotification {
pub id: NotificationId,
pub user_id: UserId,
pub read: bool,
pub created: DateTime<Utc>,
pub body: LegacyNotificationBody,
// DEPRECATED: use body field instead
#[serde(rename = "type")]
pub type_: Option<String>,
pub title: String,
pub text: String,
pub link: String,
pub actions: Vec<LegacyNotificationAction>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct LegacyNotificationAction {
pub title: String,
/// The route to call when this notification action is called. Formatted HTTP Method, route
pub action_route: (String, String),
}
#[derive(Serialize, Deserialize)]
pub enum LegacyNotificationBody {
ProjectUpdate {
project_id: ProjectId,
version_id: VersionId,
},
TeamInvite {
project_id: ProjectId,
team_id: TeamId,
invited_by: UserId,
role: String,
},
OrganizationInvite {
organization_id: OrganizationId,
invited_by: UserId,
team_id: TeamId,
role: String,
},
StatusChange {
project_id: ProjectId,
old_status: ProjectStatus,
new_status: ProjectStatus,
},
ModeratorMessage {
thread_id: ThreadId,
message_id: ThreadMessageId,
project_id: Option<ProjectId>,
report_id: Option<ReportId>,
},
LegacyMarkdown {
notification_type: Option<String>,
title: String,
text: String,
link: String,
actions: Vec<NotificationAction>,
},
Unknown,
}
impl LegacyNotification {
pub fn from(notification: Notification) -> Self {
let type_ = match &notification.body {
NotificationBody::ProjectUpdate { .. } => Some("project_update".to_string()),
NotificationBody::TeamInvite { .. } => Some("team_invite".to_string()),
NotificationBody::OrganizationInvite { .. } => Some("organization_invite".to_string()),
NotificationBody::StatusChange { .. } => Some("status_change".to_string()),
NotificationBody::ModeratorMessage { .. } => Some("moderator_message".to_string()),
NotificationBody::LegacyMarkdown {
notification_type, ..
} => notification_type.clone(),
NotificationBody::Unknown => None,
};
let legacy_body = match notification.body {
NotificationBody::ProjectUpdate {
project_id,
version_id,
} => LegacyNotificationBody::ProjectUpdate {
project_id,
version_id,
},
NotificationBody::TeamInvite {
project_id,
team_id,
invited_by,
role,
} => LegacyNotificationBody::TeamInvite {
project_id,
team_id,
invited_by,
role,
},
NotificationBody::OrganizationInvite {
organization_id,
invited_by,
team_id,
role,
} => LegacyNotificationBody::OrganizationInvite {
organization_id,
invited_by,
team_id,
role,
},
NotificationBody::StatusChange {
project_id,
old_status,
new_status,
} => LegacyNotificationBody::StatusChange {
project_id,
old_status,
new_status,
},
NotificationBody::ModeratorMessage {
thread_id,
message_id,
project_id,
report_id,
} => LegacyNotificationBody::ModeratorMessage {
thread_id,
message_id,
project_id,
report_id,
},
NotificationBody::LegacyMarkdown {
notification_type,
name,
text,
link,
actions,
} => LegacyNotificationBody::LegacyMarkdown {
notification_type,
title: name,
text,
link,
actions,
},
NotificationBody::Unknown => LegacyNotificationBody::Unknown,
};
Self {
id: notification.id,
user_id: notification.user_id,
read: notification.read,
created: notification.created,
body: legacy_body,
type_,
title: notification.name,
text: notification.text,
link: notification.link,
actions: notification
.actions
.into_iter()
.map(LegacyNotificationAction::from)
.collect(),
}
}
}
impl LegacyNotificationAction {
pub fn from(notification_action: NotificationAction) -> Self {
Self {
title: notification_action.name,
action_route: notification_action.action_route,
}
}
}

View File

@ -10,7 +10,7 @@ use crate::database::models::{version_item, DatabaseError};
use crate::database::redis::RedisPool;
use crate::models::ids::{ProjectId, VersionId};
use crate::models::projects::{
Dependency, GalleryItem, License, Link, Loader, ModeratorMessage, MonetizationStatus, Project,
Dependency, License, Link, Loader, ModeratorMessage, MonetizationStatus, Project,
ProjectStatus, Version, VersionFile, VersionStatus, VersionType,
};
use crate::models::threads::ThreadId;
@ -63,7 +63,7 @@ pub struct LegacyProject {
pub wiki_url: Option<String>,
pub discord_url: Option<String>,
pub donation_urls: Option<Vec<DonationLink>>,
pub gallery: Vec<GalleryItem>,
pub gallery: Vec<LegacyGalleryItem>,
pub color: Option<u32>,
pub thread_id: ThreadId,
pub monetization_status: MonetizationStatus,
@ -151,12 +151,12 @@ impl LegacyProject {
id: data.id,
slug: data.slug,
project_type,
team: data.team,
team: data.team_id,
organization: data.organization,
title: data.title,
description: data.description,
body: data.body,
body_url: data.body_url,
title: data.name,
description: data.summary, // V2 description is V3 summary
body: data.description, // V2 body is V3 description
body_url: None, // Always None even in V2
published: data.published,
updated: data.updated,
approved: data.approved,
@ -177,7 +177,11 @@ impl LegacyProject {
wiki_url,
discord_url,
donation_urls,
gallery: data.gallery,
gallery: data
.gallery
.into_iter()
.map(LegacyGalleryItem::from)
.collect(),
color: data.color,
thread_id: data.thread_id,
monetization_status: data.monetization_status,
@ -317,7 +321,7 @@ impl From<Version> for LegacyVersion {
name: data.name,
version_number: data.version_number,
changelog: data.changelog,
changelog_url: data.changelog_url,
changelog_url: None, // Always None even in V2
date_published: data.date_published,
downloads: data.downloads,
version_type: data.version_type,
@ -332,6 +336,29 @@ impl From<Version> for LegacyVersion {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LegacyGalleryItem {
pub url: String,
pub featured: bool,
pub name: Option<String>,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub ordering: i64,
}
impl LegacyGalleryItem {
fn from(data: crate::models::projects::GalleryItem) -> Self {
Self {
url: data.url,
featured: data.featured,
name: data.name,
description: data.description,
created: data.created,
ordering: data.ordering,
}
}
}
#[derive(Serialize, Deserialize, Validate, Clone, Eq, PartialEq)]
pub struct DonationLink {
pub id: String,

View File

@ -109,7 +109,7 @@ impl LegacyResultSearchProject {
project_id: result_search_project.project_id,
slug: result_search_project.slug,
author: result_search_project.author,
title: result_search_project.title,
title: result_search_project.name,
description: result_search_project.description,
display_categories,
downloads: result_search_project.downloads,

41
src/models/v2/teams.rs Normal file
View File

@ -0,0 +1,41 @@
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::models::{
ids::TeamId,
teams::{ProjectPermissions, TeamMember},
users::User,
};
/// A member of a team
#[derive(Serialize, Deserialize, Clone)]
pub struct LegacyTeamMember {
pub role: String,
// is_owner removed, and role hardcoded to Owner if true,
pub team_id: TeamId,
pub user: User,
pub permissions: Option<ProjectPermissions>,
pub accepted: bool,
#[serde(with = "rust_decimal::serde::float_option")]
pub payouts_split: Option<Decimal>,
pub ordering: i64,
}
impl LegacyTeamMember {
pub fn from(team_member: TeamMember) -> Self {
LegacyTeamMember {
role: match (team_member.is_owner, team_member.role.as_str()) {
(true, _) => "Owner".to_string(),
(false, "Owner") => "Member".to_string(), // The odd case of a non-owner with the owner role should show as 'Member'
(false, role) => role.to_string(),
},
team_id: team_member.team_id,
user: team_member.user,
permissions: team_member.permissions,
accepted: team_member.accepted,
payouts_split: team_member.payouts_split,
ordering: team_member.ordering,
}
}
}

View File

@ -20,7 +20,7 @@ pub struct Collection {
/// The person that has ownership of this collection.
pub user: UserId,
/// The title or name of the collection.
pub title: String,
pub name: String,
/// A short description of the collection.
pub description: String,
@ -48,7 +48,7 @@ impl From<database::models::Collection> for Collection {
id: c.id.into(),
user: c.user_id.into(),
created: c.created,
title: c.title,
name: c.name,
description: c.description,
updated: c.updated,
projects: c.projects.into_iter().map(|x| x.into()).collect(),

View File

@ -21,10 +21,7 @@ pub struct Notification {
pub created: DateTime<Utc>,
pub body: NotificationBody,
// DEPRECATED: use body field instead
#[serde(rename = "type")]
pub type_: Option<String>,
pub title: String,
pub name: String,
pub text: String,
pub link: String,
pub actions: Vec<NotificationAction>,
@ -63,7 +60,7 @@ pub enum NotificationBody {
},
LegacyMarkdown {
notification_type: Option<String>,
title: String,
name: String,
text: String,
link: String,
actions: Vec<NotificationAction>,
@ -73,13 +70,12 @@ pub enum NotificationBody {
impl From<DBNotification> for Notification {
fn from(notif: DBNotification) -> Self {
let (type_, title, text, link, actions) = {
let (name, text, link, actions) = {
match &notif.body {
NotificationBody::ProjectUpdate {
project_id,
version_id,
} => (
Some("project_update".to_string()),
"A project you follow has been updated!".to_string(),
format!(
"The project {} has released a new version: {}",
@ -94,17 +90,16 @@ impl From<DBNotification> for Notification {
team_id,
..
} => (
Some("team_invite".to_string()),
"You have been invited to join a team!".to_string(),
format!("An invite has been sent for you to be {} of a team", role),
format!("/project/{}", project_id),
vec![
NotificationAction {
title: "Accept".to_string(),
name: "Accept".to_string(),
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
},
NotificationAction {
title: "Deny".to_string(),
name: "Deny".to_string(),
action_route: (
"DELETE".to_string(),
format!("team/{team_id}/members/{}", UserId::from(notif.user_id)),
@ -118,7 +113,6 @@ impl From<DBNotification> for Notification {
team_id,
..
} => (
Some("organization_invite".to_string()),
"You have been invited to join an organization!".to_string(),
format!(
"An invite has been sent for you to be {} of an organization",
@ -127,11 +121,11 @@ impl From<DBNotification> for Notification {
format!("/organization/{}", organization_id),
vec![
NotificationAction {
title: "Accept".to_string(),
name: "Accept".to_string(),
action_route: ("POST".to_string(), format!("team/{team_id}/join")),
},
NotificationAction {
title: "Deny".to_string(),
name: "Deny".to_string(),
action_route: (
"DELETE".to_string(),
format!(
@ -147,7 +141,6 @@ impl From<DBNotification> for Notification {
new_status,
project_id,
} => (
Some("status_change".to_string()),
"Project status has changed".to_string(),
format!(
"Status has changed from {} to {}",
@ -162,7 +155,6 @@ impl From<DBNotification> for Notification {
report_id,
..
} => (
Some("moderator_message".to_string()),
"A moderator has sent you a message!".to_string(),
"Click on the link to read more.".to_string(),
if let Some(project_id) = project_id {
@ -175,25 +167,20 @@ impl From<DBNotification> for Notification {
vec![],
),
NotificationBody::LegacyMarkdown {
notification_type,
title,
name,
text,
link,
actions,
..
} => (
notification_type.clone(),
title.clone(),
name.clone(),
text.clone(),
link.clone(),
actions.clone().into_iter().map(Into::into).collect(),
),
NotificationBody::Unknown => (
None,
"".to_string(),
"".to_string(),
"#".to_string(),
vec![],
),
NotificationBody::Unknown => {
("".to_string(), "".to_string(), "#".to_string(), vec![])
}
}
};
@ -204,9 +191,7 @@ impl From<DBNotification> for Notification {
read: notif.read,
created: notif.created,
// DEPRECATED
type_,
title,
name,
text,
link,
actions,
@ -216,7 +201,7 @@ impl From<DBNotification> for Notification {
#[derive(Serialize, Deserialize, Clone)]
pub struct NotificationAction {
pub title: String,
pub name: String,
/// The route to call when this notification action is called. Formatted HTTP Method, route
pub action_route: (String, String),
}
@ -224,7 +209,7 @@ pub struct NotificationAction {
impl From<DBNotificationAction> for NotificationAction {
fn from(act: DBNotificationAction) -> Self {
Self {
title: act.title,
name: act.name,
action_route: (act.action_route_method, act.action_route),
}
}

View File

@ -16,7 +16,7 @@ pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
/// The title (and slug) of the organization
pub title: String,
pub name: String,
/// The associated team of the organization
pub team_id: TeamId,
/// The description of the organization
@ -38,7 +38,7 @@ impl Organization {
) -> Self {
Self {
id: data.id.into(),
title: data.title,
name: data.name,
team_id: data.team_id.into(),
description: data.description,
members: team_members,

View File

@ -34,17 +34,15 @@ pub struct Project {
/// The aggregated games of the versions of this project
pub games: Vec<String>,
/// The team of people that has ownership of this project.
pub team: TeamId,
pub team_id: TeamId,
/// The optional organization of people that have ownership of this project.
pub organization: Option<OrganizationId>,
/// The title or name of the project.
pub title: String,
pub name: String,
/// A short description of the project.
pub description: String,
pub summary: String,
/// A long form description of the project.
pub body: String,
/// The link to the long description of the project. Deprecated, always None
pub body_url: Option<String>,
pub description: String,
/// The date at which the project was first published.
pub published: DateTime<Utc>,
@ -151,12 +149,11 @@ impl From<QueryProject> for Project {
slug: m.slug,
project_types: data.project_types,
games: data.games,
team: m.team_id.into(),
team_id: m.team_id.into(),
organization: m.organization_id.map(|i| i.into()),
title: m.title,
name: m.name,
summary: m.summary,
description: m.description,
body: m.body,
body_url: None,
published: m.published,
updated: m.updated,
approved: m.approved,
@ -210,7 +207,7 @@ impl From<QueryProject> for Project {
.map(|x| GalleryItem {
url: x.image_url,
featured: x.featured,
title: x.title,
name: x.name,
description: x.description,
created: x.created,
ordering: x.ordering,
@ -228,7 +225,7 @@ impl From<QueryProject> for Project {
pub struct GalleryItem {
pub url: String,
pub featured: bool,
pub title: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub ordering: i64,
@ -464,8 +461,6 @@ pub struct Version {
pub games: Vec<String>,
/// The changelog for this version of the project.
pub changelog: String,
/// A link to the changelog for this version of the project. Deprecated, always None
pub changelog_url: Option<String>,
/// The date that this version was published.
pub date_published: DateTime<Utc>,
@ -517,7 +512,6 @@ impl From<QueryVersion> for Version {
project_types: data.project_types,
games: data.games,
changelog: v.changelog,
changelog_url: None,
date_published: v.date_published,
downloads: v.downloads as u32,
version_type: match v.version_type.as_str() {

View File

@ -135,6 +135,8 @@ pub struct TeamMember {
pub user: User,
/// The role of the user in the team
pub role: String,
/// Is the user the owner of the team?
pub is_owner: bool,
/// A bitset containing the user's permissions in this team.
/// In an organization-controlled project, these are the unique overriding permissions for the user's role for any project in the organization, if they exist.
/// In an organization, these are the default project permissions for any project in the organization.
@ -178,6 +180,7 @@ impl TeamMember {
team_id: data.team_id.into(),
user,
role: data.role,
is_owner: data.is_owner,
permissions: if override_permissions {
None
} else {

View File

@ -80,7 +80,7 @@ pub async fn maven_metadata(
) -> Result<HttpResponse, ApiError> {
let project_id = params.into_inner().0;
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
let user_option = get_user_from_headers(
@ -95,7 +95,7 @@ pub async fn maven_metadata(
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let version_names = sqlx::query!(
@ -274,7 +274,7 @@ pub async fn version_file(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
let user_option = get_user_from_headers(
@ -289,15 +289,15 @@ pub async fn version_file(
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
if file == format!("{}-{}.pom", &project_id, &vnum) {
@ -310,7 +310,7 @@ pub async fn version_file(
group_id: "maven.modrinth".to_string(),
artifact_id: project_id,
version: vnum,
name: project.inner.title,
name: project.inner.name,
description: project.inner.description,
};
return Ok(HttpResponse::Ok()
@ -322,7 +322,7 @@ pub async fn version_file(
.body(""));
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[get("maven/modrinth/{id}/{versionnum}/{file}.sha1")]
@ -335,7 +335,7 @@ pub async fn version_file_sha1(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
let user_option = get_user_from_headers(
@ -350,15 +350,15 @@ pub async fn version_file_sha1(
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
Ok(find_file(&project_id, &vnum, &version, &file)
@ -377,7 +377,7 @@ pub async fn version_file_sha512(
) -> Result<HttpResponse, ApiError> {
let (project_id, vnum, file) = params.into_inner();
let Some(project) = database::models::Project::get(&project_id, &**pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
let user_option = get_user_from_headers(
@ -392,15 +392,15 @@ pub async fn version_file_sha512(
.ok();
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let Some(version) = find_version(&project, &vnum, &pool, &redis).await? else {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
};
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
Ok(find_file(&project_id, &vnum, &version, &file)

View File

@ -123,6 +123,8 @@ pub enum ApiError {
Mail(#[from] crate::auth::email::MailError),
#[error("Error while rerouting request: {0}")]
Reroute(#[from] reqwest::Error),
#[error("Resource not found")]
NotFound,
}
impl actix_web::ResponseError for ApiError {
@ -150,6 +152,7 @@ impl actix_web::ResponseError for ApiError {
ApiError::PasswordStrengthCheck(..) => StatusCode::BAD_REQUEST,
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::NotFound => StatusCode::NOT_FOUND,
}
}
@ -178,6 +181,7 @@ impl actix_web::ResponseError for ApiError {
ApiError::Mail(..) => "mail_error",
ApiError::Clickhouse(..) => "clickhouse_error",
ApiError::Reroute(..) => "reroute_error",
ApiError::NotFound => "not_found",
},
description: &self.to_string(),
})

View File

@ -0,0 +1,272 @@
use super::ApiError;
use crate::database::redis::RedisPool;
use crate::routes::{v2_reroute, v3};
use crate::{models::ids::VersionId, queue::session::AuthQueue};
use actix_web::{get, web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("analytics")
.service(playtimes_get)
.service(views_get)
.service(downloads_get)
.service(revenue_get)
.service(countries_downloads_get)
.service(countries_views_get),
);
}
/// The json data to be passed to fetch analytic data
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// start_date and end_date are optional, and default to two weeks ago, and the maximum date respectively
/// start_date and end_date are inclusive
/// resolution_minutes is optional. This refers to the window by which we are looking (every day, every minute, etc) and defaults to 1440 (1 day)
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GetData {
// only one of project_ids or version_ids should be used
// if neither are provided, all projects the user has access to will be used
pub project_ids: Option<String>,
pub version_ids: Option<String>,
pub start_date: Option<DateTime<Utc>>, // defaults to 2 weeks ago
pub end_date: Option<DateTime<Utc>>, // defaults to now
pub resolution_minutes: Option<u32>, // defaults to 1 day. Ignored in routes that do not aggregate over a resolution (eg: /countries)
}
/// Get playtime data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of days to playtime data
/// eg:
/// {
/// "4N1tEhnO": {
/// "20230824": 23
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[derive(Serialize, Deserialize, Clone)]
pub struct FetchedPlaytime {
pub time: u64,
pub total_seconds: u64,
pub loader_seconds: HashMap<String, u64>,
pub game_version_seconds: HashMap<String, u64>,
pub parent_seconds: HashMap<VersionId, u64>,
}
#[get("playtime")]
pub async fn playtimes_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::playtimes_get(
req,
clickhouse,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: data.version_ids,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
}
/// Get view data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of days to views
/// eg:
/// {
/// "4N1tEhnO": {
/// "20230824": 1090
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[get("views")]
pub async fn views_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::views_get(
req,
clickhouse,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: data.version_ids,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
/// Get download data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of days to downloads
/// eg:
/// {
/// "4N1tEhnO": {
/// "20230824": 32
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
#[get("downloads")]
pub async fn downloads_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::downloads_get(
req,
clickhouse,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: data.version_ids,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
/// Get payout data for a set of projects
/// Data is returned as a hashmap of project ids to a hashmap of days to amount earned per day
/// eg:
/// {
/// "4N1tEhnO": {
/// "20230824": 0.001
/// }
///}
/// ONLY project IDs can be used. Unauthorized projects will be filtered out.
#[get("revenue")]
pub async fn revenue_get(
req: HttpRequest,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::revenue_get(
req,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: None,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
/// Get country data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to downloads.
/// Unknown countries are labeled "".
/// This is usuable to see significant performing countries per project
/// eg:
/// {
/// "4N1tEhnO": {
/// "CAN": 22
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
#[get("countries/downloads")]
pub async fn countries_downloads_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::countries_downloads_get(
req,
clickhouse,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: data.version_ids,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
/// Get country data for a set of projects or versions
/// Data is returned as a hashmap of project/version ids to a hashmap of coutnry to views.
/// Unknown countries are labeled "".
/// This is usuable to see significant performing countries per project
/// eg:
/// {
/// "4N1tEhnO": {
/// "CAN": 56165
/// }
///}
/// Either a list of project_ids or version_ids can be used, but not both. Unauthorized projects/versions will be filtered out.
/// For this endpoint, provided dates are a range to aggregate over, not specific days to fetch
#[get("countries/views")]
pub async fn countries_views_get(
req: HttpRequest,
clickhouse: web::Data<clickhouse::Client>,
data: web::Query<GetData>,
session_queue: web::Data<AuthQueue>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
let data = data.into_inner();
v3::analytics_get::countries_views_get(
req,
clickhouse,
web::Query(v3::analytics_get::GetData {
project_ids: data.project_ids,
version_ids: data.version_ids,
start_date: data.start_date,
end_date: data.end_date,
resolution_minutes: data.resolution_minutes,
}),
session_queue,
pool,
redis,
)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -0,0 +1,191 @@
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::collections::CollectionStatus;
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::CreateError;
use crate::routes::{v3, ApiError};
use actix_web::web::Data;
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(collections_get);
cfg.service(collection_create);
cfg.service(
web::scope("collection")
.service(collection_get)
.service(collection_delete)
.service(collection_edit)
.service(collection_icon_edit)
.service(delete_collection_icon),
);
}
#[derive(Serialize, Deserialize, Validate, Clone)]
pub struct CollectionCreateData {
#[validate(
length(min = 3, max = 64),
custom(function = "crate::util::validate::validate_name")
)]
/// The title or name of the project.
pub title: String,
#[validate(length(min = 3, max = 255))]
/// A short description of the collection.
pub description: String,
#[validate(length(max = 32))]
#[serde(default = "Vec::new")]
/// A list of initial projects to use with the created collection
pub projects: Vec<String>,
}
#[post("collection")]
pub async fn collection_create(
req: HttpRequest,
collection_create_data: web::Json<CollectionCreateData>,
client: Data<PgPool>,
redis: Data<RedisPool>,
session_queue: Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
let collection_create_data = collection_create_data.into_inner();
v3::collections::collection_create(
req,
web::Json(v3::collections::CollectionCreateData {
name: collection_create_data.title,
description: collection_create_data.description,
projects: collection_create_data.projects,
}),
client,
redis,
session_queue,
)
.await
}
#[derive(Serialize, Deserialize)]
pub struct CollectionIds {
pub ids: String,
}
#[get("collections")]
pub async fn collections_get(
req: HttpRequest,
web::Query(ids): web::Query<CollectionIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::collections_get(
req,
web::Query(v3::collections::CollectionIds { ids: ids.ids }),
pool,
redis,
session_queue,
)
.await
}
#[get("{id}")]
pub async fn collection_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::collection_get(req, info, pool, redis, session_queue).await
}
#[derive(Deserialize, Validate)]
pub struct EditCollection {
#[validate(
length(min = 3, max = 64),
custom(function = "crate::util::validate::validate_name")
)]
pub title: Option<String>,
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
pub status: Option<CollectionStatus>,
#[validate(length(max = 64))]
pub new_projects: Option<Vec<String>>,
}
#[patch("{id}")]
pub async fn collection_edit(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
new_collection: web::Json<EditCollection>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let new_collection = new_collection.into_inner();
v3::collections::collection_edit(
req,
info,
pool,
web::Json(v3::collections::EditCollection {
name: new_collection.title,
description: new_collection.description,
status: new_collection.status,
new_projects: new_collection.new_projects,
}),
redis,
session_queue,
)
.await
}
#[derive(Serialize, Deserialize)]
pub struct Extension {
pub ext: String,
}
#[patch("{id}/icon")]
#[allow(clippy::too_many_arguments)]
pub async fn collection_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::collection_icon_edit(
web::Query(v3::collections::Extension { ext: ext.ext }),
req,
info,
pool,
redis,
file_host,
payload,
session_queue,
)
.await
}
#[delete("{id}/icon")]
pub async fn delete_collection_icon(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::delete_collection_icon(req, info, pool, redis, file_host, session_queue).await
}
#[delete("{id}")]
pub async fn collection_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::collections::collection_delete(req, info, pool, redis, session_queue).await
}

View File

@ -1,7 +1,7 @@
use super::ApiError;
use crate::database::redis::RedisPool;
use crate::queue::session::AuthQueue;
use crate::routes::v3;
use crate::{database::redis::RedisPool, routes::v2_reroute};
use actix_web::{get, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
@ -36,4 +36,5 @@ pub async fn get_projects(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -1,6 +1,9 @@
use crate::database::redis::RedisPool;
use crate::models::ids::NotificationId;
use crate::models::notifications::Notification;
use crate::models::v2::notifications::LegacyNotification;
use crate::queue::session::AuthQueue;
use crate::routes::v2_reroute;
use crate::routes::v3;
use crate::routes::ApiError;
use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse};
@ -33,7 +36,7 @@ pub async fn notifications_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::notifications::notifications_get(
let resp = v3::notifications::notifications_get(
req,
web::Query(v3::notifications::NotificationIds { ids: ids.ids }),
pool,
@ -41,6 +44,17 @@ pub async fn notifications_get(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error);
match v2_reroute::extract_ok_json::<Vec<Notification>>(resp?).await {
Ok(notifications) => {
let notifications: Vec<LegacyNotification> = notifications
.into_iter()
.map(LegacyNotification::from)
.collect();
Ok(HttpResponse::Ok().json(notifications))
}
Err(response) => Ok(response),
}
}
#[get("{id}")]
@ -51,7 +65,16 @@ pub async fn notification_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::notifications::notification_get(req, info, pool, redis, session_queue).await
let response = v3::notifications::notification_get(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
match v2_reroute::extract_ok_json::<Notification>(response).await {
Ok(notification) => {
let notification = LegacyNotification::from(notification);
Ok(HttpResponse::Ok().json(notification))
}
Err(response) => Ok(response),
}
}
#[patch("{id}")]
@ -62,7 +85,9 @@ pub async fn notification_read(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::notifications::notification_read(req, info, pool, redis, session_queue).await
v3::notifications::notification_read(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}")]
@ -73,7 +98,9 @@ pub async fn notification_delete(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::notifications::notification_delete(req, info, pool, redis, session_queue).await
v3::notifications::notification_delete(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[patch("notifications")]
@ -92,6 +119,7 @@ pub async fn notifications_read(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("notifications")]
@ -110,4 +138,5 @@ pub async fn notifications_delete(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -0,0 +1,265 @@
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::projects::Project;
use crate::models::v2::projects::LegacyProject;
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::CreateError;
use crate::routes::{v2_reroute, v3, ApiError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::sync::Arc;
use validator::Validate;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(organizations_get).service(organization_create);
cfg.service(
web::scope("organization")
.service(organization_get)
.service(organizations_edit)
.service(organization_delete)
.service(organization_projects_get)
.service(organization_projects_add)
.service(organization_projects_remove)
.service(organization_icon_edit)
.service(delete_organization_icon)
.service(super::teams::team_members_get_organization),
);
}
#[derive(Deserialize, Validate)]
pub struct NewOrganization {
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub title: String,
#[validate(length(min = 3, max = 256))]
pub description: String,
}
#[post("organization")]
pub async fn organization_create(
req: HttpRequest,
new_organization: web::Json<NewOrganization>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, CreateError> {
let new_organization = new_organization.into_inner();
v3::organizations::organization_create(
req,
web::Json(v3::organizations::NewOrganization {
name: new_organization.title,
description: new_organization.description,
}),
pool.clone(),
redis.clone(),
session_queue,
)
.await
}
#[get("{id}")]
pub async fn organization_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::organization_get(req, info, pool.clone(), redis.clone(), session_queue).await
}
#[derive(Deserialize)]
pub struct OrganizationIds {
pub ids: String,
}
#[get("organizations")]
pub async fn organizations_get(
req: HttpRequest,
web::Query(ids): web::Query<OrganizationIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::organizations_get(
req,
web::Query(v3::organizations::OrganizationIds { ids: ids.ids }),
pool,
redis,
session_queue,
)
.await
}
#[derive(Serialize, Deserialize, Validate)]
pub struct OrganizationEdit {
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub title: Option<String>,
}
#[patch("{id}")]
pub async fn organizations_edit(
req: HttpRequest,
info: web::Path<(String,)>,
new_organization: web::Json<OrganizationEdit>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let new_organization = new_organization.into_inner();
v3::organizations::organizations_edit(
req,
info,
web::Json(v3::organizations::OrganizationEdit {
description: new_organization.description,
name: new_organization.title,
}),
pool.clone(),
redis.clone(),
session_queue,
)
.await
}
#[delete("{id}")]
pub async fn organization_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::organization_delete(req, info, pool.clone(), redis.clone(), session_queue)
.await
}
#[get("{id}/projects")]
pub async fn organization_projects_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let response = v3::organizations::organization_projects_get(
req,
info,
pool.clone(),
redis.clone(),
session_queue,
)
.await?;
// Convert v3 projects to v2
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
Ok(project) => {
let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?;
Ok(HttpResponse::Ok().json(legacy_projects))
}
Err(response) => Ok(response),
}
}
#[derive(Deserialize)]
pub struct OrganizationProjectAdd {
pub project_id: String, // Also allow title/slug
}
#[post("{id}/projects")]
pub async fn organization_projects_add(
req: HttpRequest,
info: web::Path<(String,)>,
project_info: web::Json<OrganizationProjectAdd>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let project_info = project_info.into_inner();
v3::organizations::organization_projects_add(
req,
info,
web::Json(v3::organizations::OrganizationProjectAdd {
project_id: project_info.project_id,
}),
pool.clone(),
redis.clone(),
session_queue,
)
.await
}
#[delete("{organization_id}/projects/{project_id}")]
pub async fn organization_projects_remove(
req: HttpRequest,
info: web::Path<(String, String)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::organization_projects_remove(
req,
info,
pool.clone(),
redis.clone(),
session_queue,
)
.await
}
#[derive(Serialize, Deserialize)]
pub struct Extension {
pub ext: String,
}
#[patch("{id}/icon")]
#[allow(clippy::too_many_arguments)]
pub async fn organization_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::organization_icon_edit(
web::Query(v3::organizations::Extension { ext: ext.ext }),
req,
info,
pool.clone(),
redis.clone(),
file_host,
payload,
session_queue,
)
.await
}
#[delete("{id}/icon")]
pub async fn delete_organization_icon(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::organizations::delete_organization_icon(
req,
info,
pool.clone(),
redis.clone(),
file_host,
session_queue,
)
.await
}

View File

@ -213,10 +213,10 @@ pub async fn project_create(
}
Ok(v3::project_creation::ProjectCreateData {
title: legacy_create.title,
name: legacy_create.title,
slug: legacy_create.slug,
description: legacy_create.description,
body: legacy_create.body,
summary: legacy_create.description, // Description becomes summary
description: legacy_create.body, // Body becomes description
initial_versions,
categories: legacy_create.categories,
additional_categories: legacy_create.additional_categories,

View File

@ -102,6 +102,8 @@ pub async fn project_search(
format!("game_versions:{}", val)
} else if facet.starts_with("project_type:") {
format!("project_types:{}", val)
} else if facet.starts_with("title:") {
format!("name:{}", val)
} else {
facet.to_string()
}
@ -143,7 +145,10 @@ pub async fn random_projects_get(
let count = v3::projects::RandomProjects { count: count.count };
let response =
v3::projects::random_projects_get(web::Query(count), pool.clone(), redis.clone()).await?;
v3::projects::random_projects_get(web::Query(count), pool.clone(), redis.clone())
.await
.or_else(v2_reroute::flatten_404_error)
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
Ok(project) => {
@ -170,7 +175,9 @@ pub async fn projects_get(
redis.clone(),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
@ -191,10 +198,11 @@ pub async fn project_get(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// Convert V2 data to V3 data
// Call V3 project creation
let response =
v3::projects::project_get(req, info, pool.clone(), redis.clone(), session_queue).await?;
let response = v3::projects::project_get(req, info, pool.clone(), redis.clone(), session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Project>(response).await {
@ -217,7 +225,10 @@ pub async fn project_get_check(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
v3::projects::project_get_check(info, pool, redis).await
v3::projects::project_get_check(info, pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize)]
@ -235,7 +246,10 @@ pub async fn dependency_list(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
// TODO: requires V2 conversion and tests, probably
v3::projects::dependency_list(req, info, pool, redis, session_queue).await
v3::projects::dependency_list(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Validate)]
@ -426,9 +440,9 @@ pub async fn project_edit(
}
let new_project = v3::projects::EditProject {
title: v2_new_project.title,
description: v2_new_project.description,
body: v2_new_project.body,
name: v2_new_project.title,
summary: v2_new_project.description, // Description becomes summary
description: v2_new_project.body, // Body becomes description
categories: v2_new_project.categories,
additional_categories: v2_new_project.additional_categories,
license_url: v2_new_project.license_url,
@ -453,7 +467,8 @@ pub async fn project_edit(
redis.clone(),
session_queue.clone(),
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// If client and server side were set, we will call
// the version setting route for each version to set the side types for each of them.
@ -642,6 +657,7 @@ pub async fn projects_edit(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
@ -672,6 +688,7 @@ pub async fn project_icon_edit(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}/icon")]
@ -683,7 +700,9 @@ pub async fn delete_project_icon(
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::projects::delete_project_icon(req, info, pool, redis, file_host, session_queue).await
v3::projects::delete_project_icon(req, info, pool, redis, file_host, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Validate)]
@ -714,7 +733,7 @@ pub async fn add_gallery_item(
req,
web::Query(v3::projects::GalleryCreateQuery {
featured: item.featured,
title: item.title,
name: item.title,
description: item.description,
ordering: item.ordering,
}),
@ -726,6 +745,7 @@ pub async fn add_gallery_item(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Validate)]
@ -764,7 +784,7 @@ pub async fn edit_gallery_item(
web::Query(v3::projects::GalleryEditQuery {
url: item.url,
featured: item.featured,
title: item.title,
name: item.title,
description: item.description,
ordering: item.ordering,
}),
@ -774,6 +794,7 @@ pub async fn edit_gallery_item(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
@ -801,6 +822,7 @@ pub async fn delete_gallery_item(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}")]
@ -812,7 +834,9 @@ pub async fn project_delete(
config: web::Data<SearchConfig>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::projects::project_delete(req, info, pool, redis, config, session_queue).await
v3::projects::project_delete(req, info, pool, redis, config, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[post("{id}/follow")]
@ -823,7 +847,9 @@ pub async fn project_follow(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::projects::project_follow(req, info, pool, redis, session_queue).await
v3::projects::project_follow(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}/follow")]
@ -834,5 +860,7 @@ pub async fn project_unfollow(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::projects::project_unfollow(req, info, pool, redis, session_queue).await
v3::projects::project_unfollow(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -2,7 +2,7 @@ use crate::database::redis::RedisPool;
use crate::models::ids::ImageId;
use crate::models::reports::ItemType;
use crate::queue::session::AuthQueue;
use crate::routes::{v3, ApiError};
use crate::routes::{v2_reroute, v3, ApiError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
@ -37,7 +37,9 @@ pub async fn report_create(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::reports::report_create(req, pool, body, redis, session_queue).await
v3::reports::report_create(req, pool, body, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -74,6 +76,7 @@ pub async fn reports(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -97,6 +100,7 @@ pub async fn reports_get(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("report/{id}")]
@ -107,7 +111,9 @@ pub async fn report_get(
info: web::Path<(crate::models::reports::ReportId,)>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::reports::report_get(req, pool, redis, info, session_queue).await
v3::reports::report_get(req, pool, redis, info, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize, Validate)]
@ -139,6 +145,7 @@ pub async fn report_edit(
}),
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("report/{id}")]
@ -149,5 +156,7 @@ pub async fn report_delete(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::reports::report_delete(req, pool, info, redis, session_queue).await
v3::reports::report_delete(req, pool, info, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -1,4 +1,4 @@
use crate::routes::{v3, ApiError};
use crate::routes::{v2_reroute, v3, ApiError};
use actix_web::{get, web, HttpResponse};
use sqlx::PgPool;
@ -8,5 +8,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
#[get("statistics")]
pub async fn get_stats(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
v3::statistics::get_stats(pool).await
v3::statistics::get_stats(pool)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -161,7 +161,9 @@ pub struct LicenseText {
#[get("license/{id}")]
pub async fn license_text(params: web::Path<(String,)>) -> Result<HttpResponse, ApiError> {
v3::tags::license_text(params).await
v3::tags::license_text(params)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(serde::Serialize)]
@ -192,6 +194,7 @@ pub async fn donation_platform_list(
Err(response) => response,
},
)
.or_else(v2_reroute::flatten_404_error)
}
#[get("report_type")]
@ -199,7 +202,9 @@ pub async fn report_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
v3::tags::report_type_list(pool, redis).await
v3::tags::report_type_list(pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("project_type")]
@ -207,7 +212,9 @@ pub async fn project_type_list(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
v3::tags::project_type_list(pool, redis).await
v3::tags::project_type_list(pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("side_type")]

View File

@ -1,8 +1,9 @@
use crate::database::redis::RedisPool;
use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId};
use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId, TeamMember};
use crate::models::users::UserId;
use crate::models::v2::teams::LegacyTeamMember;
use crate::queue::session::AuthQueue;
use crate::routes::{v3, ApiError};
use crate::routes::{v2_reroute, v3, ApiError};
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
@ -34,7 +35,20 @@ pub async fn team_members_get_project(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::teams::team_members_get_project(req, info, pool, redis, session_queue).await
let response = v3::teams::team_members_get_project(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<TeamMember>>(response).await {
Ok(members) => {
let members = members
.into_iter()
.map(LegacyTeamMember::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(members))
}
Err(response) => Ok(response),
}
}
// Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project)
@ -46,7 +60,20 @@ pub async fn team_members_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::teams::team_members_get(req, info, pool, redis, session_queue).await
let response = v3::teams::team_members_get(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<TeamMember>>(response).await {
Ok(members) => {
let members = members
.into_iter()
.map(LegacyTeamMember::from)
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(members))
}
Err(response) => Ok(response),
}
}
#[derive(Serialize, Deserialize)]
@ -62,7 +89,7 @@ pub async fn teams_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::teams::teams_get(
let response = v3::teams::teams_get(
req,
web::Query(v3::teams::TeamIds { ids: ids.ids }),
pool,
@ -70,6 +97,23 @@ pub async fn teams_get(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error);
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Vec<TeamMember>>>(response?).await {
Ok(members) => {
let members = members
.into_iter()
.map(|members| {
members
.into_iter()
.map(LegacyTeamMember::from)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(members))
}
Err(response) => Ok(response),
}
}
#[post("{id}/join")]
@ -80,7 +124,9 @@ pub async fn join_team(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::teams::join_team(req, info, pool, redis, session_queue).await
v3::teams::join_team(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
fn default_role() -> String {
@ -132,6 +178,7 @@ pub async fn add_team_member(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize, Clone)]
@ -167,6 +214,7 @@ pub async fn edit_team_member(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -194,6 +242,7 @@ pub async fn transfer_ownership(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}/members/{user_id}")]
@ -204,5 +253,7 @@ pub async fn remove_team_member(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::teams::remove_team_member(req, info, pool, redis, session_queue).await
v3::teams::remove_team_member(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -5,7 +5,7 @@ use crate::file_hosting::FileHost;
use crate::models::ids::ThreadMessageId;
use crate::models::threads::{MessageBody, ThreadId};
use crate::queue::session::AuthQueue;
use crate::routes::{v3, ApiError};
use crate::routes::{v2_reroute, v3, ApiError};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use serde::Deserialize;
use sqlx::PgPool;
@ -30,7 +30,9 @@ pub async fn thread_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::threads::thread_get(req, info, pool, redis, session_queue).await
v3::threads::thread_get(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -54,6 +56,7 @@ pub async fn threads_get(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -82,6 +85,7 @@ pub async fn thread_send_message(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("inbox")]
@ -91,7 +95,9 @@ pub async fn moderation_inbox(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::threads::moderation_inbox(req, pool, redis, session_queue).await
v3::threads::moderation_inbox(req, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[post("{id}/read")]
@ -102,7 +108,9 @@ pub async fn thread_read(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::threads::thread_read(req, info, pool, redis, session_queue).await
v3::threads::thread_read(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[delete("{id}")]
@ -114,5 +122,7 @@ pub async fn message_delete(
session_queue: web::Data<AuthQueue>,
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
) -> Result<HttpResponse, ApiError> {
v3::threads::message_delete(req, info, pool, redis, session_queue, file_host).await
v3::threads::message_delete(req, info, pool, redis, session_queue, file_host)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -1,7 +1,9 @@
use crate::database::redis::RedisPool;
use crate::file_hosting::FileHost;
use crate::models::notifications::Notification;
use crate::models::projects::Project;
use crate::models::users::{Badges, Role};
use crate::models::v2::notifications::LegacyNotification;
use crate::models::v2::projects::LegacyProject;
use crate::queue::session::AuthQueue;
use crate::routes::{v2_reroute, v3, ApiError};
@ -36,7 +38,9 @@ pub async fn user_auth_get(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::users::user_auth_get(req, pool, redis, session_queue).await
v3::users::user_auth_get(req, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
@ -50,7 +54,9 @@ pub async fn users_get(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
v3::users::users_get(web::Query(v3::users::UserIds { ids: ids.ids }), pool, redis).await
v3::users::users_get(web::Query(v3::users::UserIds { ids: ids.ids }), pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("{id}")]
@ -59,7 +65,9 @@ pub async fn user_get(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
) -> Result<HttpResponse, ApiError> {
v3::users::user_get(info, pool, redis).await
v3::users::user_get(info, pool, redis)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("{user_id}/projects")]
@ -70,8 +78,9 @@ pub async fn projects_list(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let response =
v3::users::projects_list(req, info, pool.clone(), redis.clone(), session_queue).await?;
let response = v3::users::projects_list(req, info, pool.clone(), redis.clone(), session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert to V2 projects
match v2_reroute::extract_ok_json::<Vec<Project>>(response).await {
@ -135,6 +144,7 @@ pub async fn user_edit(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
@ -165,6 +175,7 @@ pub async fn user_icon_edit(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Deserialize)]
@ -198,6 +209,7 @@ pub async fn user_delete(
session_queue,
)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("{id}/follows")]
@ -208,7 +220,9 @@ pub async fn user_follows(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::users::user_follows(req, info, pool, redis, session_queue).await
v3::users::user_follows(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[get("{id}/notifications")]
@ -219,5 +233,18 @@ pub async fn user_notifications(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::users::user_notifications(req, info, pool, redis, session_queue).await
let response = v3::users::user_notifications(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Notification>>(response).await {
Ok(notifications) => {
let legacy_notifications: Vec<LegacyNotification> = notifications
.into_iter()
.map(LegacyNotification::from)
.collect();
Ok(HttpResponse::Ok().json(legacy_notifications))
}
Err(response) => Ok(response),
}
}

View File

@ -3,7 +3,7 @@ use crate::database::redis::RedisPool;
use crate::models::projects::{Project, Version, VersionType};
use crate::models::v2::projects::{LegacyProject, LegacyVersion};
use crate::queue::session::AuthQueue;
use crate::routes::v3::version_file::{default_algorithm, HashQuery};
use crate::routes::v3::version_file::HashQuery;
use crate::routes::{v2_reroute, v3};
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
use serde::{Deserialize, Serialize};
@ -40,10 +40,11 @@ pub async fn get_version_from_hash(
) -> Result<HttpResponse, ApiError> {
let response =
v3::version_file::get_version_from_hash(req, info, pool, redis, hash_query, session_queue)
.await;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Version>(response?).await {
match v2_reroute::extract_ok_json::<Version>(response).await {
Ok(version) => {
let v2_version = LegacyVersion::from(version);
Ok(HttpResponse::Ok().json(v2_version))
@ -62,7 +63,9 @@ pub async fn download_version(
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::version_file::download_version(req, info, pool, redis, hash_query, session_queue).await
v3::version_file::download_version(req, info, pool, redis, hash_query, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
// under /api/v1/version_file/{hash}
@ -75,7 +78,9 @@ pub async fn delete_file(
hash_query: web::Query<HashQuery>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::version_file::delete_file(req, info, pool, redis, hash_query, session_queue).await
v3::version_file::delete_file(req, info, pool, redis, hash_query, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}
#[derive(Serialize, Deserialize)]
@ -119,7 +124,8 @@ pub async fn get_update_from_hash(
web::Json(update_data),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Version>(response).await {
@ -134,8 +140,7 @@ pub async fn get_update_from_hash(
// Requests above with multiple versions below
#[derive(Deserialize)]
pub struct FileHashes {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>,
pub hashes: Vec<String>,
}
@ -160,7 +165,8 @@ pub async fn get_versions_from_hashes(
web::Json(file_data),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert to V2
match v2_reroute::extract_ok_json::<HashMap<String, Version>>(response).await {
@ -198,7 +204,8 @@ pub async fn get_projects_from_hashes(
web::Json(file_data),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert to V2
match v2_reroute::extract_ok_json::<HashMap<String, Project>>(response).await {
@ -230,8 +237,7 @@ pub async fn get_projects_from_hashes(
#[derive(Deserialize)]
pub struct ManyUpdateData {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
pub loaders: Option<Vec<String>>,
pub game_versions: Option<Vec<String>>,
@ -265,7 +271,8 @@ pub async fn update_files(
let response =
v3::version_file::update_files(req, pool, redis, web::Json(update_data), session_queue)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<HashMap<String, Version>>(response).await {
@ -293,8 +300,7 @@ pub struct FileUpdateData {
#[derive(Deserialize)]
pub struct ManyFileUpdateData {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>,
}
@ -338,7 +344,8 @@ pub async fn update_individual_files(
web::Json(update_data),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<HashMap<String, Version>>(response).await {

View File

@ -73,7 +73,8 @@ pub async fn version_list(
let response =
v3::versions::version_list(req, info, web::Query(filters), pool, redis, session_queue)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Version>>(response).await {
@ -98,8 +99,9 @@ pub async fn version_project_get(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner();
let response =
v3::versions::version_project_get_helper(req, id, pool, redis, session_queue).await?;
let response = v3::versions::version_project_get_helper(req, id, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Version>(response).await {
Ok(version) => {
@ -124,8 +126,9 @@ pub async fn versions_get(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let ids = v3::versions::VersionIds { ids: ids.ids };
let response =
v3::versions::versions_get(req, web::Query(ids), pool, redis, session_queue).await?;
let response = v3::versions::versions_get(req, web::Query(ids), pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Vec<Version>>(response).await {
@ -149,7 +152,9 @@ pub async fn version_get(
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let id = info.into_inner().0;
let response = v3::versions::version_get_helper(req, id, pool, redis, session_queue).await?;
let response = v3::versions::version_get_helper(req, id, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)?;
// Convert response to V2 format
match v2_reroute::extract_ok_json::<Version>(response).await {
Ok(version) => {
@ -248,7 +253,8 @@ pub async fn version_edit(
web::Json(serde_json::to_value(new_version)?),
session_queue,
)
.await?;
.await
.or_else(v2_reroute::flatten_404_error)?;
Ok(response)
}
@ -260,5 +266,7 @@ pub async fn version_delete(
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
v3::versions::version_delete(req, info, pool, redis, session_queue).await
v3::versions::version_delete(req, info, pool, redis, session_queue)
.await
.or_else(v2_reroute::flatten_404_error)
}

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use super::v3::project_creation::CreateError;
use super::ApiError;
use crate::models::v2::projects::LegacySideType;
use crate::util::actix::{generate_multipart, MultipartSegment, MultipartSegmentData};
use actix_multipart::Multipart;
@ -14,6 +15,7 @@ pub async fn extract_ok_json<T>(response: HttpResponse) -> Result<T, HttpRespons
where
T: serde::de::DeserializeOwned,
{
// If the response is OK, parse the json and return it
if response.status() == actix_web::http::StatusCode::OK {
let failure_http_response = || {
HttpResponse::InternalServerError().json(json!({
@ -33,6 +35,15 @@ where
}
}
// This only removes the body of 404 responses
// This should not be used on the fallback no-route-found handler
pub fn flatten_404_error(res: ApiError) -> Result<HttpResponse, ApiError> {
match res {
ApiError::NotFound => Ok(HttpResponse::NotFound().body("")),
_ => Err(res),
}
}
pub async fn alter_actix_multipart<T, U, Fut>(
mut multipart: Multipart,
mut headers: HeaderMap,

View File

@ -43,7 +43,7 @@ pub struct CollectionCreateData {
custom(function = "crate::util::validate::validate_name")
)]
/// The title or name of the project.
pub title: String,
pub name: String,
#[validate(length(min = 3, max = 255))]
/// A short description of the collection.
pub description: String,
@ -94,7 +94,7 @@ pub async fn collection_create(
let collection_builder_actual = collection_item::CollectionBuilder {
collection_id: collection_id.into(),
user_id: current_user.id.into(),
title: collection_create_data.title,
name: collection_create_data.name,
description: collection_create_data.description,
status: CollectionStatus::Listed,
projects: initial_project_ids
@ -111,7 +111,7 @@ pub async fn collection_create(
let response = crate::models::collections::Collection {
id: collection_id,
user: collection_builder.user_id.into(),
title: collection_builder.title.clone(),
name: collection_builder.name.clone(),
description: collection_builder.description.clone(),
created: now,
updated: now,
@ -187,7 +187,7 @@ pub async fn collection_get(
return Ok(HttpResponse::Ok().json(Collection::from(data)));
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Deserialize, Validate)]
@ -196,7 +196,7 @@ pub struct EditCollection {
length(min = 3, max = 64),
custom(function = "crate::util::validate::validate_name")
)]
pub title: Option<String>,
pub name: Option<String>,
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
pub status: Option<CollectionStatus>,
@ -239,14 +239,14 @@ pub async fn collection_edit(
let mut transaction = pool.begin().await?;
if let Some(title) = &new_collection.title {
if let Some(name) = &new_collection.name {
sqlx::query!(
"
UPDATE collections
SET title = $1
SET name = $1
WHERE (id = $2)
",
title.trim(),
name.trim(),
id as database::models::ids::CollectionId,
)
.execute(&mut *transaction)
@ -335,7 +335,7 @@ pub async fn collection_edit(
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -526,7 +526,7 @@ pub async fn collection_delete(
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -93,10 +93,10 @@ pub async fn notification_get(
if user.id == data.user_id.into() || user.role.is_admin() {
Ok(HttpResponse::Ok().json(Notification::from(data)))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -142,7 +142,7 @@ pub async fn notification_read(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -188,7 +188,7 @@ pub async fn notification_delete(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -92,7 +92,7 @@ pub async fn get_user_clients(
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -108,7 +108,7 @@ pub async fn get_client(
if let Some(client) = clients.into_iter().next() {
Ok(HttpResponse::Ok().json(client))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -241,7 +241,7 @@ pub async fn oauth_client_delete<'a>(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -349,7 +349,7 @@ pub async fn oauth_client_edit(
Ok(HttpResponse::Ok().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -71,7 +71,7 @@ pub async fn organization_projects_get(
"
SELECT m.id FROM organizations o
INNER JOIN mods m ON m.organization_id = o.id
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.title = $2 AND $2 IS NOT NULL)
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.name = $2 AND $2 IS NOT NULL)
",
possible_organization_id.map(|x| x as i64),
info
@ -95,7 +95,7 @@ pub struct NewOrganization {
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub title: String,
pub name: String,
#[validate(length(min = 3, max = 256))]
pub description: String,
}
@ -124,13 +124,13 @@ pub async fn organization_create(
let mut transaction = pool.begin().await?;
// Try title
let title_organization_id_option: Option<OrganizationId> =
serde_json::from_str(&format!("\"{}\"", new_organization.title)).ok();
let name_organization_id_option: Option<OrganizationId> =
serde_json::from_str(&format!("\"{}\"", new_organization.name)).ok();
let mut organization_strings = vec![];
if let Some(title_organization_id) = title_organization_id_option {
organization_strings.push(title_organization_id.to_string());
if let Some(name_organization_id) = name_organization_id_option {
organization_strings.push(name_organization_id.to_string());
}
organization_strings.push(new_organization.title.clone());
organization_strings.push(new_organization.name.clone());
let results = Organization::get_many(&organization_strings, &mut *transaction, &redis).await?;
if !results.is_empty() {
return Err(CreateError::SlugCollision);
@ -143,6 +143,7 @@ pub async fn organization_create(
members: vec![team_item::TeamMemberBuilder {
user_id: current_user.id.into(),
role: crate::models::teams::OWNER_ROLE.to_owned(),
is_owner: true,
permissions: ProjectPermissions::all(),
organization_permissions: Some(OrganizationPermissions::all()),
accepted: true,
@ -155,7 +156,7 @@ pub async fn organization_create(
// Create organization
let organization = Organization {
id: organization_id,
title: new_organization.title.clone(),
name: new_organization.name.clone(),
description: new_organization.description.clone(),
team_id,
icon_url: None,
@ -243,7 +244,7 @@ pub async fn organization_get(
let organization = models::organizations::Organization::from(data, team_members);
return Ok(HttpResponse::Ok().json(organization));
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Deserialize)]
@ -335,7 +336,7 @@ pub struct OrganizationEdit {
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub title: Option<String>,
pub name: Option<String>,
}
pub async fn organizations_edit(
@ -397,47 +398,47 @@ pub async fn organizations_edit(
.await?;
}
if let Some(title) = &new_organization.title {
if let Some(name) = &new_organization.name {
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the title of this organization!"
"You do not have the permissions to edit the name of this organization!"
.to_string(),
));
}
let title_organization_id_option: Option<u64> = parse_base62(title).ok();
if let Some(title_organization_id) = title_organization_id_option {
let name_organization_id_option: Option<u64> = parse_base62(name).ok();
if let Some(name_organization_id) = name_organization_id_option {
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM organizations WHERE id=$1)
",
title_organization_id as i64
name_organization_id as i64
)
.fetch_one(&mut *transaction)
.await?;
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"Title collides with other organization's id!".to_string(),
"name collides with other organization's id!".to_string(),
));
}
}
// Make sure the new title is different from the old one
// We are able to unwrap here because the title is always set
if !title.eq(&organization_item.title.clone()) {
// Make sure the new name is different from the old one
// We are able to unwrap here because the name is always set
if !name.eq(&organization_item.name.clone()) {
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM organizations WHERE title = LOWER($1))
SELECT EXISTS(SELECT 1 FROM organizations WHERE name = LOWER($1))
",
title
name
)
.fetch_one(&mut *transaction)
.await?;
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"Title collides with other organization's id!".to_string(),
"Name collides with other organization's id!".to_string(),
));
}
}
@ -445,10 +446,10 @@ pub async fn organizations_edit(
sqlx::query!(
"
UPDATE organizations
SET title = LOWER($1)
SET name = LOWER($1)
WHERE (id = $2)
",
Some(title),
Some(name),
id as database::models::ids::OrganizationId,
)
.execute(&mut *transaction)
@ -457,7 +458,7 @@ pub async fn organizations_edit(
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.title),
Some(organization_item.name),
&redis,
)
.await?;
@ -470,7 +471,7 @@ pub async fn organizations_edit(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -527,19 +528,19 @@ pub async fn organization_delete(
transaction.commit().await?;
database::models::Organization::clear_cache(organization.id, Some(organization.title), &redis)
database::models::Organization::clear_cache(organization.id, Some(organization.name), &redis)
.await?;
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
#[derive(Deserialize)]
pub struct OrganizationProjectAdd {
pub project_id: String, // Also allow title/slug
pub project_id: String, // Also allow name/slug
}
pub async fn organization_projects_add(
req: HttpRequest,
@ -596,11 +597,7 @@ pub async fn organization_projects_add(
})?;
// Require ownership of a project to add it to an organization
if !current_user.role.is_admin()
&& !project_team_member
.role
.eq(crate::models::teams::OWNER_ROLE)
{
if !current_user.role.is_admin() && !project_team_member.is_owner {
return Err(ApiError::CustomAuthentication(
"You need to be an owner of a project to add it to an organization!".to_string(),
));
@ -824,7 +821,7 @@ pub async fn organization_icon_edit(
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.title),
Some(organization_item.name),
&redis,
)
.await?;
@ -909,7 +906,7 @@ pub async fn delete_organization_icon(
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.title),
Some(organization_item.name),
&redis,
)
.await?;

View File

@ -158,7 +158,7 @@ pub struct ProjectCreateData {
)]
#[serde(alias = "mod_name")]
/// The title or name of the project.
pub title: String,
pub name: String,
#[validate(
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
@ -169,11 +169,11 @@ pub struct ProjectCreateData {
#[validate(length(min = 3, max = 255))]
#[serde(alias = "mod_description")]
/// A short description of the project.
pub description: String,
pub summary: String,
#[validate(length(max = 65536))]
#[serde(alias = "mod_body")]
/// A long description of the project, in markdown.
pub body: String,
pub description: String,
#[validate(length(max = 32))]
#[validate]
@ -225,7 +225,7 @@ pub struct NewGalleryItem {
pub featured: bool,
#[validate(length(min = 1, max = 2048))]
/// The title of the gallery item
pub title: Option<String>,
pub name: Option<String>,
#[validate(length(min = 1, max = 2048))]
/// The description of the gallery item
pub description: Option<String>,
@ -518,7 +518,7 @@ async fn project_create_inner(
gallery_urls.push(crate::models::projects::GalleryItem {
url: format!("{cdn_url}/{url}"),
featured: item.featured,
title: item.title.clone(),
name: item.name.clone(),
description: item.description.clone(),
created: Utc::now(),
ordering: item.ordering,
@ -616,6 +616,7 @@ async fn project_create_inner(
members: vec![models::team_item::TeamMemberBuilder {
user_id: current_user.id.into(),
role: crate::models::teams::OWNER_ROLE.to_owned(),
is_owner: true,
// Allow all permissions for project creator, even if attached to a project
permissions: ProjectPermissions::all(),
organization_permissions: None,
@ -679,9 +680,9 @@ async fn project_create_inner(
project_id: project_id.into(),
team_id,
organization_id: project_create_data.organization_id.map(|x| x.into()),
title: project_create_data.title,
name: project_create_data.name,
summary: project_create_data.summary,
description: project_create_data.description,
body: project_create_data.body,
icon_url: icon_data.clone().map(|x| x.0),
license_url: project_create_data.license_url,
@ -698,7 +699,7 @@ async fn project_create_inner(
.map(|x| models::project_item::GalleryItem {
image_url: x.url.clone(),
featured: x.featured,
title: x.title.clone(),
name: x.name.clone(),
description: x.description.clone(),
created: x.created,
ordering: x.ordering,
@ -783,12 +784,11 @@ async fn project_create_inner(
slug: project_builder.slug.clone(),
project_types,
games,
team: team_id.into(),
team_id: team_id.into(),
organization: project_create_data.organization_id,
title: project_builder.title.clone(),
name: project_builder.name.clone(),
summary: project_builder.summary.clone(),
description: project_builder.description.clone(),
body: project_builder.body.clone(),
body_url: None,
published: now,
updated: now,
approved: None,

View File

@ -166,7 +166,7 @@ pub async fn project_get(
return Ok(HttpResponse::Ok().json(Project::from(data)));
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Serialize, Deserialize, Validate)]
@ -175,11 +175,11 @@ pub struct EditProject {
length(min = 3, max = 64),
custom(function = "crate::util::validate::validate_name")
)]
pub title: Option<String>,
pub name: Option<String>,
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
pub summary: Option<String>,
#[validate(length(max = 65536))]
pub body: Option<String>,
pub description: Option<String>,
#[validate(length(max = 3))]
pub categories: Option<Vec<String>>,
#[validate(length(max = 256))]
@ -272,10 +272,10 @@ pub async fn project_edit(
if let Some(perms) = permissions {
let mut transaction = pool.begin().await?;
if let Some(title) = &new_project.title {
if let Some(name) = &new_project.name {
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the title of this project!"
"You do not have the permissions to edit the name of this project!"
.to_string(),
));
}
@ -283,20 +283,20 @@ pub async fn project_edit(
sqlx::query!(
"
UPDATE mods
SET title = $1
SET name = $1
WHERE (id = $2)
",
title.trim(),
name.trim(),
id as db_ids::ProjectId,
)
.execute(&mut *transaction)
.await?;
}
if let Some(description) = &new_project.description {
if let Some(summary) = &new_project.summary {
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the description of this project!"
"You do not have the permissions to edit the summary of this project!"
.to_string(),
));
}
@ -304,10 +304,10 @@ pub async fn project_edit(
sqlx::query!(
"
UPDATE mods
SET description = $1
SET summary = $1
WHERE (id = $2)
",
description,
summary,
id as db_ids::ProjectId,
)
.execute(&mut *transaction)
@ -664,55 +664,57 @@ pub async fn project_edit(
.await?;
}
if let Some(links) = &new_project.link_urls {
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the links of this project!"
.to_string(),
));
}
if !links.is_empty() {
if !perms.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the links of this project!"
.to_string(),
));
}
let ids_to_delete = links
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<String>>();
// Deletes all links from hashmap- either will be deleted or be replaced
sqlx::query!(
"
DELETE FROM mods_links
WHERE joining_mod_id = $1 AND joining_platform_id IN (
SELECT id FROM link_platforms WHERE name = ANY($2)
let ids_to_delete = links
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<String>>();
// Deletes all links from hashmap- either will be deleted or be replaced
sqlx::query!(
"
DELETE FROM mods_links
WHERE joining_mod_id = $1 AND joining_platform_id IN (
SELECT id FROM link_platforms WHERE name = ANY($2)
)
",
id as db_ids::ProjectId,
&ids_to_delete
)
",
id as db_ids::ProjectId,
&ids_to_delete
)
.execute(&mut *transaction)
.await?;
.execute(&mut *transaction)
.await?;
for (platform, url) in links {
if let Some(url) = url {
let platform_id = db_models::categories::LinkPlatform::get_id(
platform,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Platform {} does not exist.",
platform.clone()
))
})?;
sqlx::query!(
"
INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)
VALUES ($1, $2, $3)
",
id as db_ids::ProjectId,
platform_id as db_ids::LinkPlatformId,
url
)
.execute(&mut *transaction)
.await?;
for (platform, url) in links {
if let Some(url) = url {
let platform_id = db_models::categories::LinkPlatform::get_id(
platform,
&mut *transaction,
)
.await?
.ok_or_else(|| {
ApiError::InvalidInput(format!(
"Platform {} does not exist.",
platform.clone()
))
})?;
sqlx::query!(
"
INSERT INTO mods_links (joining_mod_id, joining_platform_id, url)
VALUES ($1, $2, $3)
",
id as db_ids::ProjectId,
platform_id as db_ids::LinkPlatformId,
url
)
.execute(&mut *transaction)
.await?;
}
}
}
}
@ -763,10 +765,10 @@ pub async fn project_edit(
.await?;
}
if let Some(body) = &new_project.body {
if let Some(description) = &new_project.description {
if !perms.contains(ProjectPermissions::EDIT_BODY) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the body of this project!"
"You do not have the permissions to edit the description (body) of this project!"
.to_string(),
));
}
@ -774,10 +776,10 @@ pub async fn project_edit(
sqlx::query!(
"
UPDATE mods
SET body = $1
SET description = $1
WHERE (id = $2)
",
body,
description,
id as db_ids::ProjectId,
)
.execute(&mut *transaction)
@ -818,7 +820,7 @@ pub async fn project_edit(
// check new description and body for links to associated images
// if they no longer exist in the description or body, delete them
let checkable_strings: Vec<&str> = vec![&new_project.description, &new_project.body]
let checkable_strings: Vec<&str> = vec![&new_project.description, &new_project.summary]
.into_iter()
.filter_map(|x| x.as_ref().map(|y| y.as_str()))
.collect();
@ -844,7 +846,7 @@ pub async fn project_edit(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -918,7 +920,7 @@ pub async fn project_get_check(
"id": models::ids::ProjectId::from(project.inner.id)
})))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -952,7 +954,7 @@ pub async fn dependency_list(
if let Some(project) = result {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let dependencies =
@ -1000,7 +1002,7 @@ pub async fn dependency_list(
Ok(HttpResponse::Ok().json(DependencyInfo { projects, versions }))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -1125,7 +1127,7 @@ pub async fn projects_edit(
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(format!(
"You do not have the permissions to bulk edit project {}!",
project.inner.title
project.inner.name
)));
}
} else if project.inner.status.is_hidden() {
@ -1136,7 +1138,7 @@ pub async fn projects_edit(
} else {
return Err(ApiError::CustomAuthentication(format!(
"You are not a member of project {}!",
project.inner.title
project.inner.name
)));
};
}
@ -1377,7 +1379,7 @@ pub async fn project_schedule(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -1591,7 +1593,7 @@ pub async fn delete_project_icon(
pub struct GalleryCreateQuery {
pub featured: bool,
#[validate(length(min = 1, max = 255))]
pub title: Option<String>,
pub name: Option<String>,
#[validate(length(min = 1, max = 2048))]
pub description: Option<String>,
pub ordering: Option<i64>,
@ -1712,7 +1714,7 @@ pub async fn add_gallery_item(
let gallery_item = vec![db_models::project_item::GalleryItem {
image_url: file_url,
featured: item.featured,
title: item.title,
name: item.name,
description: item.description,
created: Utc::now(),
ordering: item.ordering.unwrap_or(0),
@ -1749,7 +1751,7 @@ pub struct GalleryEditQuery {
with = "::serde_with::rust::double_option"
)]
#[validate(length(min = 1, max = 255))]
pub title: Option<Option<String>>,
pub name: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
@ -1864,15 +1866,15 @@ pub async fn edit_gallery_item(
.execute(&mut *transaction)
.await?;
}
if let Some(title) = item.title {
if let Some(name) = item.name {
sqlx::query!(
"
UPDATE mods_gallery
SET title = $2
SET name = $2
WHERE id = $1
",
id,
title
name
)
.execute(&mut *transaction)
.await?;
@ -2101,7 +2103,7 @@ pub async fn project_delete(
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -2133,7 +2135,7 @@ pub async fn project_follow(
let project_id: db_ids::ProjectId = result.inner.id;
if !is_authorized(&result.inner, &Some(user), &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let following = sqlx::query!(

View File

@ -361,13 +361,13 @@ pub async fn report_get(
if let Some(report) = report {
if !user.role.is_mod() && report.reporter != user.id.into() {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let report: Report = report.into();
Ok(HttpResponse::Ok().json(report))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -401,7 +401,7 @@ pub async fn report_edit(
if let Some(report) = report {
if !user.role.is_mod() && report.reporter != user.id.into() {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let mut transaction = pool.begin().await?;
@ -479,7 +479,7 @@ pub async fn report_edit(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -519,6 +519,6 @@ pub async fn report_delete(
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -59,7 +59,7 @@ pub async fn team_members_get_project(
.ok();
if !is_authorized(&project.inner, &current_user, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let mut members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
@ -110,7 +110,7 @@ pub async fn team_members_get_project(
.collect();
Ok(HttpResponse::Ok().json(team_members))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -174,7 +174,7 @@ pub async fn team_members_get_organization(
Ok(HttpResponse::Ok().json(team_members))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -343,6 +343,7 @@ pub async fn join_team(
Some(true),
None,
None,
None,
&mut transaction,
)
.await?;
@ -474,12 +475,6 @@ pub async fn add_team_member(
}
}
if new_member.role == crate::models::teams::OWNER_ROLE {
return Err(ApiError::InvalidInput(
"The `Owner` role is restricted to one person".to_string(),
));
}
if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) {
return Err(ApiError::InvalidInput(
"Payouts split must be between 0 and 5000!".to_string(),
@ -510,6 +505,7 @@ pub async fn add_team_member(
team_id,
user_id: new_member.user_id.into(),
role: new_member.role.clone(),
is_owner: false, // Cannot just create an owner
permissions: new_member.permissions,
organization_permissions: new_member.organization_permissions,
accepted: false,
@ -598,11 +594,9 @@ pub async fn edit_team_member(
let mut transaction = pool.begin().await?;
if &*edit_member_db.role == crate::models::teams::OWNER_ROLE
&& (edit_member.role.is_some() || edit_member.permissions.is_some())
{
if edit_member_db.is_owner && edit_member.permissions.is_some() {
return Err(ApiError::InvalidInput(
"The owner's permission and role of a team cannot be edited".to_string(),
"The owner's permission's in a team cannot be edited".to_string(),
));
}
@ -683,12 +677,6 @@ pub async fn edit_team_member(
}
}
if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) {
return Err(ApiError::InvalidInput(
"The `Owner` role is restricted to one person".to_string(),
));
}
TeamMember::edit_team_member(
id,
user_id,
@ -698,6 +686,7 @@ pub async fn edit_team_member(
None,
edit_member.payouts_split,
edit_member.ordering,
None,
&mut transaction,
)
.await?;
@ -758,7 +747,7 @@ pub async fn transfer_ownership(
)
})?;
if member.role != crate::models::teams::OWNER_ROLE {
if !member.is_owner {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit the ownership of this team".to_string(),
));
@ -779,15 +768,17 @@ pub async fn transfer_ownership(
let mut transaction = pool.begin().await?;
// The following are the only places new_is_owner is modified.
TeamMember::edit_team_member(
id.into(),
current_user.id.into(),
None,
None,
Some(crate::models::teams::DEFAULT_ROLE.to_string()),
None,
None,
None,
None,
Some(false),
&mut transaction,
)
.await?;
@ -797,10 +788,11 @@ pub async fn transfer_ownership(
new_owner.user_id.into(),
Some(ProjectPermissions::all()),
Some(OrganizationPermissions::all()),
Some(crate::models::teams::OWNER_ROLE.to_string()),
None,
None,
None,
None,
Some(true),
&mut transaction,
)
.await?;
@ -841,7 +833,7 @@ pub async fn remove_team_member(
let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?;
if let Some(delete_member) = delete_member {
if delete_member.role == crate::models::teams::OWNER_ROLE {
if delete_member.is_owner {
// The owner cannot be removed from a team
return Err(ApiError::CustomAuthentication(
"The owner can't be removed from a team".to_string(),
@ -939,6 +931,6 @@ pub async fn remove_team_member(
transaction.commit().await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -263,7 +263,7 @@ pub async fn thread_get(
return Ok(HttpResponse::Ok().json(Thread::from(data, users, &user)));
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Deserialize)]
@ -371,7 +371,7 @@ pub async fn thread_send_message(
if let Some(thread) = result {
if !is_authorized_thread(&thread, &user, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let mut transaction = pool.begin().await?;
@ -499,7 +499,7 @@ pub async fn thread_send_message(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -616,6 +616,6 @@ pub async fn message_delete(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -83,7 +83,7 @@ pub async fn projects_list(
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -143,7 +143,7 @@ pub async fn user_get(
let response: crate::models::users::User = data.into();
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -186,7 +186,7 @@ pub async fn collections_list(
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -268,7 +268,7 @@ pub async fn orgs_list(
Ok(HttpResponse::Ok().json(organizations))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -458,7 +458,7 @@ pub async fn user_edit(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -536,7 +536,7 @@ pub async fn user_icon_edit(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
} else {
Err(ApiError::InvalidInput(format!(
@ -597,10 +597,10 @@ pub async fn user_delete(
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -655,7 +655,7 @@ pub async fn user_follows(
Ok(HttpResponse::Ok().json(projects))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -696,6 +696,6 @@ pub async fn user_notifications(
notifications.sort_by(|a, b| b.created.cmp(&a.created));
Ok(HttpResponse::Ok().json(notifications))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -421,7 +421,6 @@ async fn version_create_inner(
project_types: all_project_types,
games: all_games,
changelog: builder.changelog.clone(),
changelog_url: None,
date_published: Utc::now(),
downloads: 0,
version_type: version_data.release_channel,

View File

@ -52,8 +52,12 @@ pub async fn get_version_from_hash(
.map(|x| x.1)
.ok();
let hash = info.into_inner().0.to_lowercase();
let algorithm = hash_query
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&[hash.clone()]));
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
algorithm,
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
@ -64,26 +68,36 @@ pub async fn get_version_from_hash(
let version = database::models::Version::get(file.version_id, &**pool, &redis).await?;
if let Some(version) = version {
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
Ok(HttpResponse::Ok().json(models::projects::Version::from(version)))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
#[derive(Serialize, Deserialize)]
pub struct HashQuery {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub version_id: Option<VersionId>,
}
pub fn default_algorithm() -> String {
// Calculates whether or not to use sha1 or sha512 based on the size of the hash
pub fn default_algorithm_from_hashes(hashes: &[String]) -> String {
// Gets first hash, optionally
let empty_string = "".into();
let hash = hashes.first().unwrap_or(&empty_string);
let hash_len = hash.len();
// Sha1 = 40 characters
// Sha512 = 128 characters
// Favour sha1 as default, unless the hash is longer or equal to 128 characters
if hash_len >= 128 {
return "sha512".into();
}
"sha1".into()
}
@ -122,7 +136,10 @@ pub async fn get_update_from_hash(
let hash = info.into_inner().0.to_lowercase();
if let Some(file) = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
hash_query
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&[hash.clone()])),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
@ -163,7 +180,7 @@ pub async fn get_update_from_hash(
if let Some(first) = versions.last() {
if !is_authorized_version(&first.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
return Ok(HttpResponse::Ok().json(models::projects::Version::from(first)));
@ -171,14 +188,13 @@ pub async fn get_update_from_hash(
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
// Requests above with multiple versions below
#[derive(Deserialize)]
pub struct FileHashes {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
}
@ -200,8 +216,13 @@ pub async fn get_versions_from_hashes(
.map(|x| x.1)
.ok();
let algorithm = file_data
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&file_data.hashes));
let files = database::models::Version::get_files_from_hash(
file_data.algorithm.clone(),
algorithm.clone(),
&file_data.hashes,
&**pool,
&redis,
@ -220,7 +241,7 @@ pub async fn get_versions_from_hashes(
for version in versions_data {
for file in files.iter().filter(|x| x.version_id == version.id.into()) {
if let Some(hash) = file.hashes.get(&file_data.algorithm) {
if let Some(hash) = file.hashes.get(&algorithm) {
response.insert(hash.clone(), version.clone());
}
}
@ -247,8 +268,12 @@ pub async fn get_projects_from_hashes(
.map(|x| x.1)
.ok();
let algorithm = file_data
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&file_data.hashes));
let files = database::models::Version::get_files_from_hash(
file_data.algorithm.clone(),
algorithm.clone(),
&file_data.hashes,
&**pool,
&redis,
@ -268,7 +293,7 @@ pub async fn get_projects_from_hashes(
for project in projects_data {
for file in files.iter().filter(|x| x.project_id == project.id.into()) {
if let Some(hash) = file.hashes.get(&file_data.algorithm) {
if let Some(hash) = file.hashes.get(&algorithm) {
response.insert(hash.clone(), project.clone());
}
}
@ -279,8 +304,7 @@ pub async fn get_projects_from_hashes(
#[derive(Deserialize)]
pub struct ManyUpdateData {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<String>,
pub loaders: Option<Vec<String>>,
pub loader_fields: Option<HashMap<String, Vec<serde_json::Value>>>,
@ -304,8 +328,12 @@ pub async fn update_files(
.map(|x| x.1)
.ok();
let algorithm = update_data
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&update_data.hashes));
let files = database::models::Version::get_files_from_hash(
update_data.algorithm.clone(),
algorithm.clone(),
&update_data.hashes,
&**pool,
&redis,
@ -366,7 +394,7 @@ pub async fn update_files(
if let Some(version) = version {
if is_authorized_version(&version.inner, &user_option, &pool).await? {
if let Some(hash) = file.hashes.get(&update_data.algorithm) {
if let Some(hash) = file.hashes.get(&algorithm) {
response.insert(
hash.clone(),
models::projects::Version::from(version.clone()),
@ -390,8 +418,7 @@ pub struct FileUpdateData {
#[derive(Serialize, Deserialize)]
pub struct ManyFileUpdateData {
#[serde(default = "default_algorithm")]
pub algorithm: String,
pub algorithm: Option<String>, // Defaults to calculation based on size of hash
pub hashes: Vec<FileUpdateData>,
}
@ -413,8 +440,17 @@ pub async fn update_individual_files(
.map(|x| x.1)
.ok();
let algorithm = update_data.algorithm.clone().unwrap_or_else(|| {
default_algorithm_from_hashes(
&update_data
.hashes
.iter()
.map(|x| x.hash.clone())
.collect::<Vec<_>>(),
)
});
let files = database::models::Version::get_files_from_hash(
update_data.algorithm.clone(),
algorithm.clone(),
&update_data
.hashes
.iter()
@ -445,7 +481,7 @@ pub async fn update_individual_files(
for project in projects {
for file in files.iter().filter(|x| x.project_id == project.inner.id) {
if let Some(hash) = file.hashes.get(&update_data.algorithm) {
if let Some(hash) = file.hashes.get(&algorithm) {
if let Some(query_file) = update_data.hashes.iter().find(|x| &x.hash == hash) {
let version = all_versions
.iter()
@ -514,9 +550,12 @@ pub async fn delete_file(
.1;
let hash = info.into_inner().0.to_lowercase();
let algorithm = hash_query
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&[hash.clone()]));
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
@ -605,7 +644,7 @@ pub async fn delete_file(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -635,8 +674,12 @@ pub async fn download_version(
.ok();
let hash = info.into_inner().0.to_lowercase();
let algorithm = hash_query
.algorithm
.clone()
.unwrap_or_else(|| default_algorithm_from_hashes(&[hash.clone()]));
let file = database::models::Version::get_file_from_hash(
hash_query.algorithm.clone(),
algorithm.clone(),
hash,
hash_query.version_id.map(|x| x.into()),
&**pool,
@ -649,16 +692,16 @@ pub async fn download_version(
if let Some(version) = version {
if !is_authorized_version(&version.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
Ok(HttpResponse::TemporaryRedirect()
.append_header(("Location", &*file.url))
.json(DownloadRedirect { url: file.url }))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -82,7 +82,7 @@ pub async fn version_project_get_helper(
if let Some(project) = result {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let versions =
@ -100,7 +100,7 @@ pub async fn version_project_get_helper(
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Serialize, Deserialize)]
@ -174,7 +174,7 @@ pub async fn version_get_helper(
}
}
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
#[derive(Serialize, Deserialize, Validate, Default, Debug)]
@ -678,7 +678,7 @@ pub async fn version_edit_helper(
))
}
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -723,7 +723,7 @@ pub async fn version_list(
if let Some(project) = result {
if !is_authorized(&project.inner, &user_option, &pool).await? {
return Ok(HttpResponse::NotFound().body(""));
return Err(ApiError::NotFound);
}
let loader_field_filters = filters.loader_fields.as_ref().map(|x| {
@ -822,7 +822,7 @@ pub async fn version_list(
Ok(HttpResponse::Ok().json(response))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -924,7 +924,7 @@ pub async fn version_schedule(
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}
@ -1010,6 +1010,6 @@ pub async fn version_delete(
if result.is_some() {
Ok(HttpResponse::NoContent().body(""))
} else {
Ok(HttpResponse::NotFound().body(""))
Err(ApiError::NotFound)
}
}

View File

@ -62,7 +62,7 @@ pub async fn index_local(
GROUP BY version_id
)
SELECT m.id id, v.id version_id, m.title title, m.description description, m.downloads downloads, m.follows follows,
SELECT m.id id, v.id version_id, m.name name, 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,
u.username username,
@ -87,7 +87,7 @@ pub async fn index_local(
LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id
LEFT JOIN games g ON lptg.game_id = g.id
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 team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE
INNER JOIN users u ON tm.user_id = u.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
@ -97,7 +97,6 @@ pub async fn index_local(
",
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
&*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::<Vec<String>>(),
crate::models::teams::OWNER_ROLE,
)
.fetch_many(&pool)
.try_filter_map(|e| {
@ -147,7 +146,7 @@ pub async fn index_local(
UploadSearchProject {
version_id: version_id.to_string(),
project_id: project_id.to_string(),
title: m.title,
name: m.name,
description: m.description,
categories,
follows: m.follows,

View File

@ -185,7 +185,7 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
"project_types",
"slug",
"author",
"title",
"name",
"description",
"categories",
"display_categories",
@ -201,7 +201,7 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[
"color",
];
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["title", "description", "author", "slug"];
const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = &["name", "description", "author", "slug"];
const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
"categories",
@ -210,7 +210,7 @@ const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[
"downloads",
"follows",
"author",
"title",
"name",
"date_created",
"created_timestamp",
"date_modified",

View File

@ -79,7 +79,7 @@ pub struct UploadSearchProject {
pub project_types: Vec<String>,
pub slug: Option<String>,
pub author: String,
pub title: String,
pub name: String,
pub description: String,
pub categories: Vec<String>,
pub display_categories: Vec<String>,
@ -119,7 +119,7 @@ pub struct ResultSearchProject {
pub project_types: Vec<String>,
pub slug: Option<String>,
pub author: String,
pub title: String,
pub name: String,
pub description: String,
pub categories: Vec<String>,
pub display_categories: Vec<String>,

View File

@ -85,7 +85,7 @@ pub async fn send_discord_webhook(
let row =
sqlx::query!(
"
SELECT m.id id, m.title title, m.description description, m.color color,
SELECT m.id id, m.name name, m.description description, m.color color,
m.icon_url icon_url, m.slug slug,
u.username username, u.avatar_url avatar_url,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null) categories,
@ -136,7 +136,7 @@ pub async fn send_discord_webhook(
LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id
LEFT JOIN games g ON lptg.game_id = g.id
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 team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE 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
@ -147,7 +147,6 @@ pub async fn send_discord_webhook(
",
project_id.0 as i64,
&*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::<Vec<String>>(),
crate::models::teams::OWNER_ROLE,
)
.fetch_optional(pool)
.await?;
@ -279,7 +278,7 @@ pub async fn send_discord_webhook(
project_type,
project.slug.unwrap_or_else(|| project_id.to_string())
),
title: project.title,
title: project.name, // Do not change DiscordEmbed
description: project.description,
timestamp: Utc::now(),
color: project.color.unwrap_or(0x1bd96a) as u32,

View File

@ -11,6 +11,7 @@ use crate::common::{api_v2::ApiV2, api_v3::ApiV3, dummy_data::TestFile};
use super::{
models::{CommonImageData, CommonProject, CommonVersion},
request_data::ProjectCreationRequestData,
Api, ApiProject, ApiTags, ApiTeams, ApiVersion,
};
@ -65,6 +66,8 @@ delegate_api_variant!(
#[async_trait(?Send)]
impl ApiProject for GenericApi {
[add_public_project, (CommonProject, Vec<CommonVersion>), slug: &str, version_jar: Option<TestFile>, modify_json: Option<json_patch::Patch>, pat: &str],
[get_public_project_creation_data_json, serde_json::Value, slug: &str, version_jar: Option<&TestFile>],
[create_project, ServiceResponse, creation_data: ProjectCreationRequestData, pat: &str],
[remove_project, ServiceResponse, project_slug_or_id: &str, pat: &str],
[get_project, ServiceResponse, id_or_slug: &str, pat: &str],
[get_project_deserialized_common, CommonProject, id_or_slug: &str, pat: &str],

View File

@ -4,6 +4,7 @@ use self::models::{
CommonCategoryData, CommonImageData, CommonLoaderData, CommonNotification, CommonProject,
CommonTeamMember, CommonVersion,
};
use self::request_data::ProjectCreationRequestData;
use actix_web::dev::ServiceResponse;
use async_trait::async_trait;
use labrinth::{
@ -18,6 +19,7 @@ use super::dummy_data::TestFile;
pub mod generic;
pub mod models;
pub mod request_data;
#[async_trait(?Send)]
pub trait ApiBuildable: Api {
async fn build(labrinth_config: LabrinthConfig) -> Self;
@ -38,6 +40,17 @@ pub trait ApiProject {
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> (CommonProject, Vec<CommonVersion>);
async fn create_project(
&self,
creation_data: ProjectCreationRequestData,
pat: &str,
) -> ServiceResponse;
async fn get_public_project_creation_data_json(
&self,
slug: &str,
version_jar: Option<&TestFile>,
) -> serde_json::Value;
async fn remove_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse;
async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse;
async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject;

View File

@ -1,12 +1,12 @@
use chrono::{DateTime, Utc};
use labrinth::models::{
notifications::{NotificationAction, NotificationBody, NotificationId},
notifications::NotificationId,
organizations::OrganizationId,
projects::{
Dependency, GalleryItem, License, ModeratorMessage, MonetizationStatus, ProjectId,
ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType,
},
teams::{OrganizationPermissions, ProjectPermissions, TeamId},
teams::{ProjectPermissions, TeamId},
threads::ThreadId,
users::{User, UserId},
};
@ -31,12 +31,7 @@ pub struct CommonProject {
// For any tests that require those fields, we make a separate test with separate API functions tht do not use Common models.
pub id: ProjectId,
pub slug: Option<String>,
pub team: TeamId,
pub organization: Option<OrganizationId>,
pub title: String,
pub description: String,
pub body: String,
pub body_url: Option<String>,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub approved: Option<DateTime<Utc>>,
@ -67,7 +62,6 @@ pub struct CommonVersion {
pub name: String,
pub version_number: String,
pub changelog: String,
pub changelog_url: Option<String>,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: VersionType,
@ -109,9 +103,7 @@ pub struct CommonTeamMember {
pub user: User,
pub role: String,
// TODO: Should these be removed from the Common?
pub permissions: Option<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
pub accepted: bool,
pub payouts_split: Option<Decimal>,
@ -124,13 +116,13 @@ pub struct CommonNotification {
pub user_id: UserId,
pub read: bool,
pub created: DateTime<Utc>,
pub body: NotificationBody,
// DEPRECATED: use body field instead
#[serde(rename = "type")]
pub type_: Option<String>,
pub title: String,
// Body is absent as one of the variants differs
pub text: String,
pub link: String,
pub actions: Vec<NotificationAction>,
pub actions: Vec<CommonNotificationAction>,
}
#[derive(Deserialize)]
pub struct CommonNotificationAction {
pub action_route: (String, String),
}

View File

@ -0,0 +1,24 @@
// The structures for project/version creation.
// These are created differently, but are essentially the same between versions.
use labrinth::util::actix::MultipartSegment;
use crate::common::dummy_data::TestFile;
pub struct ProjectCreationRequestData {
pub slug: String,
pub jar: Option<TestFile>,
pub segment_data: Vec<MultipartSegment>,
}
pub struct VersionCreationRequestData {
pub version: String,
pub jar: Option<TestFile>,
pub segment_data: Vec<MultipartSegment>,
}
pub struct ImageData {
pub filename: String,
pub extension: String,
pub icon: Vec<u8>,
}

View File

@ -1,6 +1,7 @@
use crate::common::{
api_common::{
models::{CommonImageData, CommonProject, CommonVersion},
request_data::ProjectCreationRequestData,
Api, ApiProject,
},
dummy_data::TestFile,
@ -20,7 +21,10 @@ use serde_json::json;
use crate::common::{asserts::assert_status, database::MOD_USER_PAT};
use super::{request_data::get_public_project_creation_data, ApiV2};
use super::{
request_data::{self, get_public_project_creation_data},
ApiV2,
};
impl ApiV2 {
pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> LegacyProject {
@ -80,17 +84,13 @@ impl ApiProject for ApiV2 {
let creation_data = get_public_project_creation_data(slug, version_jar, modify_json);
// Add a project.
let req = TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", pat))
.set_multipart(creation_data.segment_data)
.to_request();
let resp = self.call(req).await;
let slug = creation_data.slug.clone();
let resp = self.create_project(creation_data, pat).await;
assert_status(&resp, StatusCode::OK);
// Approve as a moderator.
let req = TestRequest::patch()
.uri(&format!("/v2/project/{}", creation_data.slug))
.uri(&format!("/v2/project/{}", slug))
.append_header(("Authorization", MOD_USER_PAT))
.set_json(json!(
{
@ -101,13 +101,11 @@ impl ApiProject for ApiV2 {
let resp = self.call(req).await;
assert_status(&resp, StatusCode::NO_CONTENT);
let project = self
.get_project_deserialized_common(&creation_data.slug, pat)
.await;
let project = self.get_project_deserialized_common(&slug, pat).await;
// Get project's versions
let req = TestRequest::get()
.uri(&format!("/v2/project/{}/version", creation_data.slug))
.uri(&format!("/v2/project/{}/version", slug))
.append_header(("Authorization", pat))
.to_request();
let resp = self.call(req).await;
@ -116,6 +114,27 @@ impl ApiProject for ApiV2 {
(project, versions)
}
async fn get_public_project_creation_data_json(
&self,
slug: &str,
version_jar: Option<&TestFile>,
) -> serde_json::Value {
request_data::get_public_project_creation_data_json(slug, version_jar)
}
async fn create_project(
&self,
creation_data: ProjectCreationRequestData,
pat: &str,
) -> ServiceResponse {
let req = TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", pat))
.set_multipart(creation_data.segment_data)
.to_request();
self.call(req).await
}
async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse {
let req = test::TestRequest::delete()
.uri(&format!("/v2/project/{project_slug_or_id}"))
@ -137,7 +156,11 @@ impl ApiProject for ApiV2 {
async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject {
let resp = self.get_project(id_or_slug, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let project: LegacyProject = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(project).unwrap();
serde_json::from_value(value).unwrap()
}
async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse {
@ -155,7 +178,11 @@ impl ApiProject for ApiV2 {
) -> Vec<CommonProject> {
let resp = self.get_user_projects(user_id_or_username, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let projects: Vec<LegacyProject> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(projects).unwrap();
serde_json::from_value(value).unwrap()
}
async fn edit_project(

View File

@ -1,30 +1,15 @@
#![allow(dead_code)]
use serde_json::json;
use crate::common::dummy_data::{DummyImage, TestFile};
use crate::common::{
api_common::request_data::{ImageData, ProjectCreationRequestData, VersionCreationRequestData},
dummy_data::{DummyImage, TestFile},
};
use labrinth::{
models::projects::ProjectId,
util::actix::{MultipartSegment, MultipartSegmentData},
};
pub struct ProjectCreationRequestData {
pub slug: String,
pub jar: Option<TestFile>,
pub segment_data: Vec<MultipartSegment>,
}
pub struct VersionCreationRequestData {
pub version: String,
pub jar: Option<TestFile>,
pub segment_data: Vec<MultipartSegment>,
}
pub struct ImageData {
pub filename: String,
pub extension: String,
pub icon: Vec<u8>,
}
pub fn get_public_project_creation_data(
slug: &str,
version_jar: Option<TestFile>,

View File

@ -72,7 +72,11 @@ impl ApiTags for ApiV2 {
async fn get_loaders_deserialized_common(&self) -> Vec<CommonLoaderData> {
let resp = self.get_loaders().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: Vec<LoaderData> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn get_categories(&self) -> ServiceResponse {
@ -86,6 +90,10 @@ impl ApiTags for ApiV2 {
async fn get_categories_deserialized_common(&self) -> Vec<CommonCategoryData> {
let resp = self.get_categories().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: Vec<CategoryData> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
}

View File

@ -1,7 +1,10 @@
use actix_http::StatusCode;
use actix_web::{dev::ServiceResponse, test};
use async_trait::async_trait;
use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions};
use labrinth::models::{
teams::{OrganizationPermissions, ProjectPermissions},
v2::{notifications::LegacyNotification, teams::LegacyTeamMember},
};
use serde_json::json;
use crate::common::{
@ -14,6 +17,38 @@ use crate::common::{
use super::ApiV2;
impl ApiV2 {
pub async fn get_organization_members_deserialized(
&self,
id_or_title: &str,
pat: &str,
) -> Vec<LegacyTeamMember> {
let resp = self.get_organization_members(id_or_title, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_team_members_deserialized(
&self,
team_id: &str,
pat: &str,
) -> Vec<LegacyTeamMember> {
let resp = self.get_team_members(team_id, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_user_notifications_deserialized(
&self,
user_id: &str,
pat: &str,
) -> Vec<LegacyNotification> {
let resp = self.get_user_notifications(user_id, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
}
#[async_trait(?Send)]
impl ApiTeams for ApiV2 {
async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse {
@ -31,6 +66,9 @@ impl ApiTeams for ApiV2 {
) -> Vec<CommonTeamMember> {
let resp = self.get_team_members(id_or_title, pat).await;
assert_eq!(resp.status(), 200);
// TODO: Note, this does NOT deserialize to any other struct first, as currently TeamMember is the same in v2 and v3.
// CommonTeamMember = TeamMember (v3)
// This may yet change, so we should keep common struct.
test::read_body_json(resp).await
}
@ -49,6 +87,9 @@ impl ApiTeams for ApiV2 {
) -> Vec<CommonTeamMember> {
let resp = self.get_project_members(id_or_title, pat).await;
assert_eq!(resp.status(), 200);
// TODO: Note, this does NOT deserialize to any other struct first, as currently TeamMember is the same in v2 and v3.
// CommonTeamMember = TeamMember (v3)
// This may yet change, so we should keep common struct.
test::read_body_json(resp).await
}
@ -67,6 +108,9 @@ impl ApiTeams for ApiV2 {
) -> Vec<CommonTeamMember> {
let resp = self.get_organization_members(id_or_title, pat).await;
assert_eq!(resp.status(), 200);
// TODO: Note, this does NOT deserialize to any other struct first, as currently TeamMember is the same in v2 and v3.
// CommonTeamMember = TeamMember (v3)
// This may yet change, so we should keep common struct.
test::read_body_json(resp).await
}
@ -132,7 +176,11 @@ impl ApiTeams for ApiV2 {
) -> Vec<CommonNotification> {
let resp = self.get_user_notifications(user_id, pat).await;
assert_status(&resp, StatusCode::OK);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: Vec<LegacyNotification> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse {

View File

@ -133,7 +133,11 @@ impl ApiVersion for ApiV2 {
)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: LegacyVersion = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse {
@ -147,7 +151,11 @@ impl ApiVersion for ApiV2 {
async fn get_version_deserialized_common(&self, id: &str, pat: &str) -> CommonVersion {
let resp = self.get_version(id, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: LegacyVersion = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn edit_version(
@ -186,7 +194,11 @@ impl ApiVersion for ApiV2 {
) -> CommonVersion {
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: LegacyVersion = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn get_versions_from_hashes(
@ -214,7 +226,11 @@ impl ApiVersion for ApiV2 {
) -> HashMap<String, CommonVersion> {
let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: HashMap<String, LegacyVersion> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn get_update_from_hash(
@ -253,7 +269,11 @@ impl ApiVersion for ApiV2 {
.get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: LegacyVersion = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn update_files(
@ -299,7 +319,11 @@ impl ApiVersion for ApiV2 {
)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: HashMap<String, LegacyVersion> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
// TODO: Not all fields are tested currently in the V2 tests, only the v2-v3 relevant ones are
@ -378,7 +402,11 @@ impl ApiVersion for ApiV2 {
)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: Vec<LegacyVersion> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
async fn edit_version_ordering(
@ -415,6 +443,10 @@ impl ApiVersion for ApiV2 {
) -> Vec<CommonVersion> {
let resp = self.get_versions(version_ids, pat).await;
assert_status(&resp, StatusCode::OK);
test::read_body_json(resp).await
// First, deserialize to the non-common format (to test the response is valid for this api version)
let v: Vec<LegacyVersion> = test::read_body_json(resp).await;
// Then, deserialize to the common format
let value = serde_json::to_value(v).unwrap();
serde_json::from_value(value).unwrap()
}
}

View File

@ -6,9 +6,9 @@ use bytes::Bytes;
use labrinth::models::{organizations::Organization, v3::projects::Project};
use serde_json::json;
use crate::common::api_common::Api;
use crate::common::api_common::{request_data::ImageData, Api};
use super::{request_data::ImageData, ApiV3};
use super::ApiV3;
impl ApiV3 {
pub async fn create_organization(
@ -21,7 +21,7 @@ impl ApiV3 {
.uri("/v3/organization")
.append_header(("Authorization", pat))
.set_json(json!({
"title": organization_title,
"name": organization_title,
"description": description,
}))
.to_request();

Some files were not shown because too many files have changed in this diff Show More