You've already forked AstralRinth
forked from xxxOFFxxx/AstralRinth
Implement more database methods and basic API routes (#50)
* feat: Implement more database methods & add mod and version routes * feat: Implement deleting mods/versions & implement categories * feat: Implement routes for categories, game versions & loaders * feat: Reorganize API routes in a (hopefully) usable way
This commit is contained in:
2
.env
2
.env
@@ -1,7 +1,7 @@
|
||||
DEBUG=true
|
||||
RUST_LOG=info,sqlx::query=warn
|
||||
|
||||
CDN_URL=cdn.modrinth.com
|
||||
CDN_URL=https://cdn.modrinth.com
|
||||
|
||||
DATABASE_URL=postgresql://labrinth@localhost/labrinth
|
||||
MEILISEARCH_ADDR=http://localhost:7700
|
||||
|
||||
6
migrations/20200730223151_more-not-null.sql
Normal file
6
migrations/20200730223151_more-not-null.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE versions
|
||||
ALTER COLUMN mod_id SET NOT NULL;
|
||||
|
||||
ALTER TABLE release_channels
|
||||
ALTER COLUMN channel SET NOT NULL;
|
||||
5
migrations/20200812183213_unique-loaders.sql
Normal file
5
migrations/20200812183213_unique-loaders.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE game_versions
|
||||
ADD UNIQUE(version);
|
||||
|
||||
ALTER TABLE loaders
|
||||
ADD UNIQUE(loader);
|
||||
804
sqlx-data.json
804
sqlx-data.json
@@ -1,5 +1,31 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"1524c0462be70077736ac70fcd037fbf75651456b692e2ce40fa2e3fc8123984": {
|
||||
"query": "\n SELECT hashes.algorithm, hashes.hash FROM hashes\n WHERE hashes.file_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "algorithm",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "hash",
|
||||
"type_info": "Bytea"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"15b2a2f1bbbbab4f1d99e5e428b2ffba77c83814b936fa6e10e2703b207f6e9a": {
|
||||
"query": "\n INSERT INTO team_members (id, team_id, user_id, member_name, role)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
@@ -16,6 +42,26 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"1b74bdb59773ffd2a78a56e4d920bb83c322e180e6174c741d4bb722c353de43": {
|
||||
"query": "\n INSERT INTO loaders (loader)\n VALUES ($1)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"1ffce9b2d5c9fa6c8b9abce4bad9f9419c44ad6367b7463b979c91b9b5b4fea1": {
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)",
|
||||
"describe": {
|
||||
@@ -36,6 +82,26 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"25131559cb73a088000ab6379a769233440ade6c7511542da410065190d203fc": {
|
||||
"query": "\n SELECT id FROM loaders\n WHERE loader = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"29e657d26f0fb24a766f5b5eb6a94d01d1616884d8ca10e91536e974d5b585a6": {
|
||||
"query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n ",
|
||||
"describe": {
|
||||
@@ -49,49 +115,74 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"320b24c5ec3c7e71a4088a2862fb02b31a3d3cfc331ccd60d73dfd49af3e53c0": {
|
||||
"query": "\n SELECT *\n FROM versions\n WHERE id = $1\n ",
|
||||
"2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": {
|
||||
"query": "\n SELECT category FROM categories\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": {
|
||||
"query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"35272854c6aeb743218e73ccf6f34427ab72f25492dfa752f87a50e3da7204c5": {
|
||||
"query": "\n SELECT v.mod_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n release_channels.channel\n FROM versions v\n INNER JOIN release_channels ON v.release_channel = release_channels.id\n WHERE v.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 1,
|
||||
"ordinal": 0,
|
||||
"name": "mod_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 2,
|
||||
"name": "version_number",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 3,
|
||||
"name": "changelog_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 4,
|
||||
"name": "date_published",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 5,
|
||||
"name": "downloads",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "release_channel",
|
||||
"type_info": "Int4"
|
||||
"ordinal": 6,
|
||||
"name": "channel",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -103,7 +194,6 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
@@ -111,6 +201,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"398ac436f5fe2f6a66544204b9ff01ae1ea1204edf03ffc16de657a861cfe0ba": {
|
||||
"query": "\n DELETE FROM categories\n WHERE category = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"4411f2aefd43881450da34db81e826110ac86c3a6cef9fd6a3e9e341508d1f09": {
|
||||
"query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"449920c44d498adf8b771973d6034dc97e1c7f3ff4d9d23599af432f294ed564": {
|
||||
"query": "\n INSERT INTO files (id, version_id, url, filename)\n VALUES ($1, $2, $3, $4)\n ",
|
||||
"describe": {
|
||||
@@ -146,6 +268,26 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"4c9e2190e2a68ffc093a69aaa1fc9384957138f57ac9cd85cbc6179613c13a08": {
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"560c3ba57c965c3ebdbe393b062da8a30a8a7116a9bace2aa7de2e8431fe0bc7": {
|
||||
"query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ",
|
||||
"describe": {
|
||||
@@ -159,6 +301,262 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"56cb9274e92f185dee3accf69cca2e34c035efbef908baefeb60548fb14e02bd": {
|
||||
"query": "\n INSERT INTO categories (category)\n VALUES ($1)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"59cf9d085593887595ea45246291f2cd64fc6677d551e96bdb60c09ff1eebf99": {
|
||||
"query": "\n SELECT files.id, files.url, files.filename FROM files\n WHERE files.version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "filename",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"5aaae159c75c9385f4d969338bce509852d4b3e3ae9d4c4e366055b5b499b19a": {
|
||||
"query": "\n SELECT v.mod_id, v.name, v.version_number,\n v.changelog_url, v.date_published, v.downloads,\n v.release_channel\n FROM versions v\n WHERE v.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "mod_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "version_number",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "changelog_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "date_published",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "downloads",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "release_channel",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"6b28cb8b54ef57c9b6f03607611f688455f0e2b27eb5deda5a8cbc5b506b4602": {
|
||||
"query": "\n DELETE FROM mods\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"72d6b5f2f11d88981db82c7247c9e7e5ebfd8d34985a1a8209d6628e66490f37": {
|
||||
"query": "\n SELECT id FROM categories\n WHERE category = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"73bdd6c9e7cd8c1ed582261aebdee0f8fd2734e712ef288a2608564c918009cb": {
|
||||
"query": "\n DELETE FROM versions WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"89fbff6249b248d3e150879aaea1662140bcb10d5104992c784285322c8b3b94": {
|
||||
"query": "\n SELECT version FROM game_versions\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": {
|
||||
"query": "\n DELETE FROM loaders\n WHERE loader = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"96d7b2c8b7b69fc370bb1a2d4a449f972eb3893dad5d6c59e498663cfc93a5c3": {
|
||||
"query": "\n SELECT title, description, downloads,\n icon_url, body_url, published,\n issues_url, source_url, wiki_url,\n team_id\n FROM mods\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "downloads",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "icon_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "body_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "published",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "issues_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "source_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "wiki_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "team_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"a55925860b4a46af864a8c38f942d7cdd85c00638e761b9696de0bf47335173b": {
|
||||
"query": "\n SELECT mod_id, version_number\n FROM versions\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
@@ -185,8 +583,8 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"b133dbf99fbf7b02e0e7ebd7948445bec2ce952ea0f926575fae0af07913d8b6": {
|
||||
"query": "\n SELECT *\n FROM versions\n WHERE id = $1\n ",
|
||||
"a5d47fb171b0a1ba322125e7cedebf5af9c5831c319bbc4f8f087cb63322bee3": {
|
||||
"query": "\n SELECT files.id, files.url, files.filename FROM files\n WHERE files.version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -196,38 +594,13 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "mod_id",
|
||||
"type_info": "Int8"
|
||||
"name": "url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"name": "filename",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "version_number",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "changelog_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "date_published",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "downloads",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "release_channel",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -236,17 +609,48 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"a647c282a276b63f36d2d8a253c32d0f627cea9cab8eb1b32b39875536bdfcbb": {
|
||||
"query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"b0e3d1c70b87bb54819e3fac04b684a9b857aeedb4dcb7cb400c2af0dbb12922": {
|
||||
"query": "\n DELETE FROM teams\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"b903ac4e686ef85ba28d698c668da07860e7f276b261d8f2cebb74e73b094970": {
|
||||
"query": "\n DELETE FROM hashes\n WHERE EXISTS(\n SELECT 1 FROM files WHERE\n (files.version_id = $1) AND\n (hashes.file_id = files.id)\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"b9399840dbbf807a03d69b7fcb3bd479ef20920ab1e3c91706a1c2c7089f48e7": {
|
||||
"query": "\n INSERT INTO teams (id)\n VALUES ($1)\n ",
|
||||
"describe": {
|
||||
@@ -294,6 +698,58 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"bec1612d4929d143bc5d6860a57cc036c5ab23e69d750ca5791c620297953c50": {
|
||||
"query": "\n SELECT team_id FROM mods WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "team_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"bee1abe8313d17a56d93b06a31240e338c3973bc7a7374799ced3df5e38d3134": {
|
||||
"query": "\n DELETE FROM game_versions_versions gvv\n WHERE gvv.joining_version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"bee1e8b7f3588c6b0534443775f3d0d66d960e96a5ae8422aa96a69238f375a4": {
|
||||
"query": "\n INSERT INTO game_versions (version)\n VALUES ($1)\n RETURNING id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"c0899dcff4d7bc1ba3e953e5099210316bff2f98e6ab77ba84bc612eac4bce0a": {
|
||||
"query": "\n SELECT gv.version FROM versions\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id=versions.id\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE versions.mod_id = $1\n ",
|
||||
"describe": {
|
||||
@@ -314,6 +770,46 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"c1fddbf97350871b79cb0c235b1f7488c6616b7c1dfbde76a712fd57e91ba158": {
|
||||
"query": "\n SELECT id FROM game_versions\n WHERE version = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"c64c487b56a25b252ff070fe03a7416e84260df8a6f938a018cc768598e9435b": {
|
||||
"query": "\n SELECT category FROM categories\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"c82eb1b059b62444ab1d17e5a0bd7ef8acea4b05c6f3576c07d20c4ca7635a11": {
|
||||
"query": "\n INSERT INTO dependencies (dependent_id, dependency_id)\n VALUES ($1, $2)\n ",
|
||||
"describe": {
|
||||
@@ -327,6 +823,26 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"c9d63ed46799db7c30a7e917d97a5d4b2b78b0234cce49e136fa57526b38c1ca": {
|
||||
"query": "\n SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"cb57ae673f1a7e50cc319efddb9bdc82e2251596bcf85aea52e8def343e423b8": {
|
||||
"query": "\n INSERT INTO hashes (file_id, algorithm, hash)\n VALUES ($1, $2, $3)\n ",
|
||||
"describe": {
|
||||
@@ -341,6 +857,24 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"cc8b672c2733bfd110ed3361c6f477b185b530228c7206cb641dbaa40e41ea9f": {
|
||||
"query": "\n SELECT loader FROM loaders\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "loader",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"ccd913bb2f3006ffe881ce2fc4ef1e721d18fe2eed6ac62627046c955129610c": {
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)",
|
||||
"describe": {
|
||||
@@ -361,6 +895,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"d12bc07adb4dc8147d0ddccd72a4f23ed38cd31d7db3d36ebbe2c9b627130f0b": {
|
||||
"query": "\n DELETE FROM team_members\n WHERE team_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"d1866ecc161c3fe3fbe094289510e99b17de563957e1f824c347c1e6ac40c40c": {
|
||||
"query": "\n SELECT loader FROM loaders\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "loader",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"d6453e50041b5521fa9e919a9162e533bb9426f8c584d98474c6ad414db715c8": {
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||
"describe": {
|
||||
@@ -381,24 +947,68 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"e0e1671ae27b7ade3e9fa340e9f98b5388f51412fe892f904f31deb40634a0e0": {
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM versions WHERE version_number=$1)",
|
||||
"d8b4e7e382c77a05395124d5a6a27cccb687d0e2c31b76d49b03aa364d099d42": {
|
||||
"query": "\n DELETE FROM files\n WHERE files.version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"deb81673526789bca38d39e64303f61d2a63febfdfb68136e58517af9f7792bc": {
|
||||
"query": "\n SELECT category FROM mods_categories\n INNER JOIN categories ON joining_category_id = id\n WHERE joining_mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"e35fa345b43725309b976efffbc8f9e20a62a5e90a86a82a77b55c39c168d2de": {
|
||||
"query": "\n SELECT id FROM versions\n WHERE mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"e673006d1355fa91ba5739d7cf569eec5e1ec501f7b1dc2b431f0b1c25ac07d5": {
|
||||
"query": "\n DELETE FROM game_versions\n WHERE version = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"e7d0a64a08df6783c942f2fcadd94dd45f8d96ad3d3736e52ce90f68d396cdab": {
|
||||
@@ -460,6 +1070,26 @@
|
||||
"nullable": []
|
||||
}
|
||||
},
|
||||
"ebf2d1fbcd12816799b60be6e8dec606eadd96edc26a840a411b44a19dc0497c": {
|
||||
"query": "\n SELECT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "loader",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"efe1bc80203f608226fa33e44654b681cc4430cec63bf7cf09b5281ff8c1c437": {
|
||||
"query": "\n SELECT m.id, m.title, m.description, m.downloads, m.icon_url, m.body_url, m.published FROM mods m\n ",
|
||||
"describe": {
|
||||
@@ -514,6 +1144,66 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"f0db9d8606ccc2196a9cfafe0e7090dab42bf790f25e0469b8947fac1cf043d5": {
|
||||
"query": "\n SELECT version FROM game_versions\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"f0dd4e10e7c5c4c27ee84be6010919a1b23cb9438ff869c1902849874c75a4af": {
|
||||
"query": "\n SELECT loaders.loader FROM loaders\n INNER JOIN loaders_versions ON loaders.id = loaders_versions.loader_id\n WHERE loaders_versions.version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "loader",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"f80ca292323952d10dbd26d3453ced5c12bdd1b71dcd3cb3ade4c7d4dc3590f6": {
|
||||
"query": "\n SELECT gv.version FROM game_versions_versions gvv\n INNER JOIN game_versions gv ON gvv.game_version_id=gv.id\n WHERE gvv.joining_version_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "version",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
},
|
||||
"fb6178b27856ff583039a974173efe5d6be4e347b6cc1d4904cf750a40d1b77f": {
|
||||
"query": "\n SELECT dependency_id id FROM dependencies\n WHERE dependent_id = $1\n ",
|
||||
"describe": {
|
||||
|
||||
396
src/database/models/categories.rs
Normal file
396
src/database/models/categories.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use super::ids::*;
|
||||
use super::DatabaseError;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
pub struct Loader {
|
||||
pub id: LoaderId,
|
||||
pub loader: String,
|
||||
}
|
||||
|
||||
pub struct GameVersion {
|
||||
pub id: GameVersionId,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub struct Category {
|
||||
pub id: CategoryId,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
pub struct CategoryBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn builder() -> CategoryBuilder<'static> {
|
||||
CategoryBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM categories
|
||||
WHERE category = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| CategoryId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: CategoryId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM categories
|
||||
WHERE id = $1
|
||||
",
|
||||
id as CategoryId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.category)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM categories
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM categories
|
||||
WHERE category = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CategoryBuilder<'a> {
|
||||
/// The name of the category. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(mut self, name: &'a str) -> Result<CategoryBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<CategoryId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO categories (category)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(CategoryId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoaderBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn builder() -> LoaderBuilder<'static> {
|
||||
LoaderBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM loaders
|
||||
WHERE loader = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| LoaderId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: LoaderId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT loader FROM loaders
|
||||
WHERE id = $1
|
||||
",
|
||||
id as LoaderId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.loader)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT loader FROM loaders
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM loaders
|
||||
WHERE loader = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LoaderBuilder<'a> {
|
||||
/// The name of the loader. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(mut self, name: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<LoaderId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO loaders (loader)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(LoaderId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GameVersionBuilder<'a> {
|
||||
pub version: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl GameVersion {
|
||||
pub fn builder() -> GameVersionBuilder<'static> {
|
||||
GameVersionBuilder { version: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(
|
||||
version: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<GameVersionId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(version.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM game_versions
|
||||
WHERE version = $1
|
||||
",
|
||||
version
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| GameVersionId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: VersionId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE id = $1
|
||||
",
|
||||
id as VersionId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.version)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM game_versions
|
||||
WHERE version = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GameVersionBuilder<'a> {
|
||||
/// The game version. Spaces must be replaced with '_' for it to be valid
|
||||
pub fn version(mut self, version: &'a str) -> Result<GameVersionBuilder<'a>, DatabaseError> {
|
||||
if version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
Ok(Self {
|
||||
version: Some(version),
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(version.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<GameVersionId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO game_versions (version)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.version
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(GameVersionId(result.id))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::DatabaseError;
|
||||
use crate::models::ids::random_base62;
|
||||
use crate::models::ids::random_base62_rng;
|
||||
use sqlx_macros::Type;
|
||||
|
||||
const ID_RETRY_COUNT: usize = 20;
|
||||
@@ -9,8 +9,10 @@ macro_rules! generate_ids {
|
||||
$vis async fn $function_name(
|
||||
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<$return_type, DatabaseError> {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let length = $id_length;
|
||||
let mut id = random_base62(length);
|
||||
let mut id = random_base62_rng(&mut rng, length);
|
||||
let mut retry_count = 0;
|
||||
|
||||
// Check if ID is unique
|
||||
@@ -20,7 +22,7 @@ macro_rules! generate_ids {
|
||||
.await?;
|
||||
|
||||
if results.exists.unwrap_or(true) {
|
||||
id = random_base62(length);
|
||||
id = random_base62_rng(&mut rng, length);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod categories;
|
||||
pub mod ids;
|
||||
pub mod mod_item;
|
||||
pub mod team_item;
|
||||
@@ -22,4 +23,9 @@ pub enum DatabaseError {
|
||||
DatabaseError(#[from] sqlx::error::Error),
|
||||
#[error("Error while trying to generate random ID")]
|
||||
RandomIdError,
|
||||
#[error(
|
||||
"Invalid identifier: Category/version names must contain only ASCII \
|
||||
alphanumeric characters or '_-'."
|
||||
)]
|
||||
InvalidIdentifier(String),
|
||||
}
|
||||
|
||||
@@ -105,4 +105,175 @@ impl Mod {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(id: ModId, executor: E) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT title, description, downloads,
|
||||
icon_url, body_url, published,
|
||||
issues_url, source_url, wiki_url,
|
||||
team_id
|
||||
FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Mod {
|
||||
id,
|
||||
team_id: TeamId(row.team_id),
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
downloads: row.downloads,
|
||||
body_url: row.body_url,
|
||||
icon_url: row.icon_url,
|
||||
published: row.published,
|
||||
issues_url: row.issues_url,
|
||||
source_url: row.source_url,
|
||||
wiki_url: row.wiki_url,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
exec: E,
|
||||
) -> Result<Option<()>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT team_id FROM mods WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
let team_id: TeamId = if let Some(id) = result {
|
||||
TeamId(id.team_id)
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_categories
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
let versions: Vec<VersionId> = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
for version in versions {
|
||||
super::Version::remove_full(version, exec).await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM team_members
|
||||
WHERE team_id = $1
|
||||
",
|
||||
team_id as TeamId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM teams
|
||||
WHERE id = $1
|
||||
",
|
||||
team_id as TeamId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = Self::get(id, executor).await?;
|
||||
if let Some(inner) = result {
|
||||
use futures::TryStreamExt;
|
||||
let categories: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM mods_categories
|
||||
INNER JOIN categories ON joining_category_id = id
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let versions: Vec<VersionId> = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
Ok(Some(QueryMod {
|
||||
inner,
|
||||
categories,
|
||||
versions,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryMod {
|
||||
pub inner: Mod,
|
||||
|
||||
pub categories: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::categories::{GameVersion, Loader};
|
||||
use super::ids::*;
|
||||
use super::DatabaseError;
|
||||
|
||||
@@ -173,7 +174,118 @@ impl Version {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_dependencies<'a, E>(&self, exec: E) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
// TODO: someone verify this
|
||||
pub async fn remove_full<'a, E>(id: VersionId, exec: E) -> Result<Option<()>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
if !result.exists.unwrap_or(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM game_versions_versions gvv
|
||||
WHERE gvv.joining_version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM loaders_versions
|
||||
WHERE loaders_versions.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| VersionFile {
|
||||
id: FileId(c.id),
|
||||
version_id: id,
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<VersionFile>>()
|
||||
.await?;
|
||||
|
||||
for file in files {
|
||||
// TODO: store backblaze id in database so that we can delete the files here
|
||||
// For now, we can't delete the files since we don't have the backblaze id
|
||||
log::warn!(
|
||||
"Can't delete version file id: {} (url: {}, name: {})",
|
||||
file.id.0,
|
||||
file.url,
|
||||
file.filename
|
||||
)
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM hashes
|
||||
WHERE EXISTS(
|
||||
SELECT 1 FROM files WHERE
|
||||
(files.version_id = $1) AND
|
||||
(hashes.file_id = files.id)
|
||||
)
|
||||
",
|
||||
id as VersionId
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM versions WHERE id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_dependencies<'a, E>(
|
||||
id: VersionId,
|
||||
exec: E,
|
||||
) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@@ -184,7 +296,7 @@ impl Version {
|
||||
SELECT dependency_id id FROM dependencies
|
||||
WHERE dependent_id = $1
|
||||
",
|
||||
self.id as VersionId,
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) })
|
||||
@@ -193,20 +305,177 @@ impl Version {
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub async fn get_mod_versions<'a, E>(
|
||||
mod_id: ModId,
|
||||
exec: E,
|
||||
) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let vec = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
mod_id as ModId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(
|
||||
id: VersionId,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT v.mod_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel
|
||||
FROM versions v
|
||||
WHERE v.id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Version {
|
||||
id,
|
||||
mod_id: ModId(row.mod_id),
|
||||
name: row.name,
|
||||
version_number: row.version_number,
|
||||
changelog_url: row.changelog_url,
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
release_channel: ChannelId(row.release_channel),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: VersionId,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryVersion>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT v.mod_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
release_channels.channel
|
||||
FROM versions v
|
||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||
WHERE v.id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
use futures::TryStreamExt;
|
||||
use sqlx::Row;
|
||||
|
||||
let game_versions: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT gv.version FROM game_versions_versions gvv
|
||||
INNER JOIN game_versions gv ON gvv.game_version_id=gv.id
|
||||
WHERE gvv.joining_version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let loaders: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT loaders.loader FROM loaders
|
||||
INNER JOIN loaders_versions ON loaders.id = loaders_versions.loader_id
|
||||
WHERE loaders_versions.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| QueryFile {
|
||||
id: FileId(c.id),
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
hashes: std::collections::HashMap::new(),
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<QueryFile>>()
|
||||
.await?;
|
||||
|
||||
for file in files.iter_mut() {
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT hashes.algorithm, hashes.hash FROM hashes
|
||||
WHERE hashes.file_id = $1
|
||||
",
|
||||
file.id as FileId
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| (c.algorithm, c.hash))) })
|
||||
.try_collect::<Vec<(String, Vec<u8>)>>()
|
||||
.await?;
|
||||
|
||||
file.hashes.extend(files);
|
||||
}
|
||||
|
||||
Ok(Some(QueryVersion {
|
||||
id,
|
||||
mod_id: ModId(row.mod_id),
|
||||
name: row.name,
|
||||
version_number: row.version_number,
|
||||
changelog_url: row.changelog_url,
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
|
||||
release_channel: row.channel,
|
||||
files: Vec::<QueryFile>::new(),
|
||||
loaders,
|
||||
game_versions,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReleaseChannel {
|
||||
pub id: ChannelId,
|
||||
pub channel: String,
|
||||
}
|
||||
pub struct Loader {
|
||||
pub id: LoaderId,
|
||||
pub loader: String,
|
||||
}
|
||||
pub struct GameVersion {
|
||||
pub id: GameVersionId,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub struct VersionFile {
|
||||
pub id: FileId,
|
||||
@@ -221,7 +490,24 @@ pub struct FileHash {
|
||||
pub hash: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Category {
|
||||
pub id: CategoryId,
|
||||
pub category: String,
|
||||
pub struct QueryVersion {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog_url: Option<String>,
|
||||
pub date_published: chrono::DateTime<chrono::Utc>,
|
||||
pub downloads: i32,
|
||||
|
||||
pub release_channel: String,
|
||||
pub files: Vec<QueryFile>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct QueryFile {
|
||||
pub id: FileId,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub hashes: std::collections::HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
@@ -176,10 +176,11 @@ async fn main() -> std::io::Result<()> {
|
||||
.data(file_host.clone())
|
||||
.data(indexing_queue.clone())
|
||||
.service(routes::index_get)
|
||||
.service(routes::mod_search)
|
||||
.service(routes::mod_create)
|
||||
.service(routes::version_create)
|
||||
.service(routes::upload_file_to_version)
|
||||
.service(
|
||||
web::scope("/api/v1/")
|
||||
.configure(routes::tags_config)
|
||||
.configure(routes::mods_config),
|
||||
)
|
||||
.default_service(web::get().to(routes::not_found))
|
||||
})
|
||||
.bind(dotenv::var("BIND_ADDR").unwrap())?
|
||||
|
||||
@@ -171,7 +171,7 @@ pub mod base62_impl {
|
||||
|
||||
fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||
let mut num: u64 = 0;
|
||||
for c in string.chars().rev() {
|
||||
for c in string.chars() {
|
||||
let next_digit;
|
||||
if c.is_ascii_digit() {
|
||||
next_digit = (c as u8 - b'0') as u64;
|
||||
|
||||
@@ -81,20 +81,13 @@ pub struct Version {
|
||||
/// A single mod file, with a url for the file and the file's hash
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct VersionFile {
|
||||
/// A list of hashes of the file
|
||||
pub hashes: Vec<FileHash>,
|
||||
/// A map of hashes of the file. The key is the hashing algorithm
|
||||
/// and the value is the string version of the hash.
|
||||
pub hashes: std::collections::HashMap<String, String>,
|
||||
/// A direct link to the file for downloading it.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// A hash of a mod's file
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FileHash {
|
||||
// TODO: decide specific algorithms
|
||||
/// The hashing algorithm used for this hash; could be "md5", "sha1", etc
|
||||
pub algorithm: String,
|
||||
/// The file hash, using the specified algorithm
|
||||
pub hash: String,
|
||||
/// A direct link to the file for downloading it.
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
|
||||
@@ -1,12 +1,64 @@
|
||||
use actix_web::web;
|
||||
|
||||
mod index;
|
||||
mod mod_creation;
|
||||
mod mods;
|
||||
mod not_found;
|
||||
mod tags;
|
||||
mod version_creation;
|
||||
mod versions;
|
||||
|
||||
pub use tags::config as tags_config;
|
||||
|
||||
pub use self::index::index_get;
|
||||
pub use self::mod_creation::mod_create;
|
||||
pub use self::mods::mod_search;
|
||||
pub use self::not_found::not_found;
|
||||
pub use self::version_creation::upload_file_to_version;
|
||||
pub use self::version_creation::version_create;
|
||||
|
||||
pub fn mods_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(mods::mod_search);
|
||||
cfg.service(mod_creation::mod_create);
|
||||
|
||||
cfg.service(
|
||||
web::scope("mod")
|
||||
.service(mods::mod_get)
|
||||
.service(mods::mod_delete)
|
||||
.service(web::scope("{mod_id}").configure(versions_config)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn versions_config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(versions::version_list)
|
||||
.service(version_creation::version_create)
|
||||
.service(
|
||||
web::scope("version")
|
||||
.service(versions::version_get)
|
||||
.service(versions::version_delete)
|
||||
.service(
|
||||
web::scope("{version_id}").service(version_creation::upload_file_to_version),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error("Internal server error")]
|
||||
DatabaseError(#[from] crate::database::models::DatabaseError),
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for ApiError {
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> actix_web::web::HttpResponse {
|
||||
actix_web::web::HttpResponse::build(self.status_code()).json(
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::DatabaseError(..) => "database_error",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ pub enum CreateError {
|
||||
InvalidIconFormat(String),
|
||||
#[error("Error with multipart data: {0}")]
|
||||
InvalidInput(String),
|
||||
#[error("Invalid game version: {0}")]
|
||||
InvalidGameVersion(String),
|
||||
#[error("Invalid loader: {0}")]
|
||||
InvalidLoader(String),
|
||||
#[error("Invalid category: {0}")]
|
||||
InvalidCategory(String),
|
||||
}
|
||||
|
||||
impl actix_web::ResponseError for CreateError {
|
||||
@@ -50,6 +56,9 @@ impl actix_web::ResponseError for CreateError {
|
||||
CreateError::MissingValueError(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidIconFormat(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidInput(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidGameVersion(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidLoader(..) => StatusCode::BAD_REQUEST,
|
||||
CreateError::InvalidCategory(..) => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +74,9 @@ impl actix_web::ResponseError for CreateError {
|
||||
CreateError::MissingValueError(..) => "invalid_input",
|
||||
CreateError::InvalidIconFormat(..) => "invalid_input",
|
||||
CreateError::InvalidInput(..) => "invalid_input",
|
||||
CreateError::InvalidGameVersion(..) => "invalid_input",
|
||||
CreateError::InvalidLoader(..) => "invalid_input",
|
||||
CreateError::InvalidCategory(..) => "invalid_input",
|
||||
},
|
||||
description: &self.to_string(),
|
||||
})
|
||||
@@ -112,7 +124,7 @@ pub async fn undo_uploads(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("api/v1/mod")]
|
||||
#[post("mod")]
|
||||
pub async fn mod_create(
|
||||
payload: Multipart,
|
||||
client: Data<PgPool>,
|
||||
@@ -256,6 +268,22 @@ async fn mod_create_inner(
|
||||
VersionType::Alpha => models::ChannelId(5),
|
||||
};
|
||||
|
||||
let mut game_versions = Vec::with_capacity(version_data.game_versions.len());
|
||||
for v in &version_data.game_versions {
|
||||
let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?;
|
||||
game_versions.push(id);
|
||||
}
|
||||
|
||||
let mut loaders = Vec::with_capacity(version_data.loaders.len());
|
||||
for l in &version_data.loaders {
|
||||
let id = models::categories::Loader::get_id(&l.0, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidLoader(l.0.clone()))?;
|
||||
loaders.push(id);
|
||||
}
|
||||
|
||||
let version = models::version_item::VersionBuilder {
|
||||
version_id: version_id.into(),
|
||||
mod_id: mod_id.into(),
|
||||
@@ -268,9 +296,8 @@ async fn mod_create_inner(
|
||||
.iter()
|
||||
.map(|x| (*x).into())
|
||||
.collect::<Vec<_>>(),
|
||||
// TODO: add game_versions and loaders info
|
||||
game_versions: vec![],
|
||||
loaders: vec![],
|
||||
game_versions,
|
||||
loaders,
|
||||
release_channel,
|
||||
};
|
||||
|
||||
@@ -329,6 +356,14 @@ async fn mod_create_inner(
|
||||
)));
|
||||
};
|
||||
|
||||
let mut categories = Vec::with_capacity(create_data.categories.len());
|
||||
for category in &create_data.categories {
|
||||
let id = models::categories::Category::get_id(&category, &mut *transaction)
|
||||
.await?
|
||||
.ok_or_else(|| CreateError::InvalidCategory(category.clone()))?;
|
||||
categories.push(id);
|
||||
}
|
||||
|
||||
let body_url = format!("data/{}/body.md", mod_id);
|
||||
|
||||
let upload_data = file_host
|
||||
@@ -367,8 +402,7 @@ async fn mod_create_inner(
|
||||
source_url: create_data.source_url,
|
||||
wiki_url: create_data.wiki_url,
|
||||
|
||||
// TODO: convert `create_data.categories` from Vec<String> to Vec<CategoryId>
|
||||
categories: Vec::new(),
|
||||
categories,
|
||||
initial_versions: created_versions,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,68 @@
|
||||
use super::ApiError;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use crate::models::mods::SearchRequest;
|
||||
use crate::search::{search_for_mod, SearchError};
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use actix_web::{delete, get, web, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[get("api/v1/mod")]
|
||||
#[get("mod")]
|
||||
pub async fn mod_search(
|
||||
web::Query(info): web::Query<SearchRequest>,
|
||||
) -> Result<HttpResponse, SearchError> {
|
||||
let results = search_for_mod(&info).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
#[get("{id}")]
|
||||
pub async fn mod_get(
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.0;
|
||||
let mod_data = database::models::Mod::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = mod_data {
|
||||
let m = data.inner;
|
||||
let response = models::mods::Mod {
|
||||
id: m.id.into(),
|
||||
team: m.team_id.into(),
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
body_url: m.body_url,
|
||||
published: m.published,
|
||||
|
||||
downloads: m.downloads as u32,
|
||||
categories: data.categories,
|
||||
versions: data.versions.into_iter().map(|v| v.into()).collect(),
|
||||
icon_url: m.icon_url,
|
||||
issues_url: m.issues_url,
|
||||
source_url: m.source_url,
|
||||
wiki_url: m.wiki_url,
|
||||
};
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This really needs auth
|
||||
// TODO: The mod remains in meilisearch's index until the index is deleted
|
||||
#[delete("{id}")]
|
||||
pub async fn mod_delete(
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.0;
|
||||
let result = database::models::Mod::remove_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
153
src/routes/tags.rs
Normal file
153
src/routes/tags.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use super::ApiError;
|
||||
use crate::database::models;
|
||||
use actix_web::{delete, get, put, web, HttpResponse};
|
||||
use models::categories::{Category, GameVersion, Loader};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/tag/")
|
||||
.service(category_list)
|
||||
.service(category_create)
|
||||
.service(category_delete)
|
||||
.service(loader_list)
|
||||
.service(loader_create)
|
||||
.service(loader_delete)
|
||||
.service(game_version_list)
|
||||
.service(game_version_create)
|
||||
.service(game_version_delete),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: searching / filtering? Could be used to implement a live
|
||||
// searching category list
|
||||
#[get("category")]
|
||||
pub async fn category_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Category::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
// At some point this may take more info, but it should be able to
|
||||
// remain idempotent
|
||||
// TODO: don't fail if category already exists
|
||||
#[put("category/{name}")]
|
||||
pub async fn category_create(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = category.into_inner().0;
|
||||
|
||||
let _id = Category::builder().name(&name)?.insert(&**pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("category/{name}")]
|
||||
pub async fn category_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
category: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = category.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = Category::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("loader")]
|
||||
pub async fn loader_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = Loader::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
// At some point this may take more info, but it should be able to
|
||||
// remain idempotent
|
||||
// TODO: don't fail if loader already exists
|
||||
#[put("loader/{name}")]
|
||||
pub async fn loader_create(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = loader.into_inner().0;
|
||||
|
||||
let _id = Loader::builder().name(&name)?.insert(&**pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("loader/{name}")]
|
||||
pub async fn loader_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
loader: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = loader.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = Loader::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("game_version")]
|
||||
pub async fn game_version_list(pool: web::Data<PgPool>) -> Result<HttpResponse, ApiError> {
|
||||
let results = GameVersion::list(&**pool).await?;
|
||||
Ok(HttpResponse::Ok().json(results))
|
||||
}
|
||||
|
||||
// At some point this may take more info, but it should be able to
|
||||
// remain idempotent
|
||||
#[put("game_version/{name}")]
|
||||
pub async fn game_version_create(
|
||||
pool: web::Data<PgPool>,
|
||||
game_version: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = game_version.into_inner().0;
|
||||
|
||||
let _id = GameVersion::builder()
|
||||
.version(&name)?
|
||||
.insert(&**pool)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[delete("game_version/{name}")]
|
||||
pub async fn game_version_delete(
|
||||
pool: web::Data<PgPool>,
|
||||
game_version: web::Path<(String,)>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let name = game_version.into_inner().0;
|
||||
let mut transaction = pool.begin().await.map_err(models::DatabaseError::from)?;
|
||||
|
||||
let result = GameVersion::remove(&name, &mut transaction).await?;
|
||||
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.map_err(models::DatabaseError::from)?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ use sqlx::postgres::PgPool;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InitialVersionData {
|
||||
pub mod_id: ModId,
|
||||
pub file_parts: Vec<String>,
|
||||
pub version_number: String,
|
||||
pub version_title: String,
|
||||
@@ -30,8 +29,10 @@ struct InitialFileData {
|
||||
// TODO: hashes?
|
||||
}
|
||||
|
||||
#[post("api/v1/version")]
|
||||
// under `/api/v1/mod/{mod_id}`
|
||||
#[post("version")]
|
||||
pub async fn version_create(
|
||||
url_data: actix_web::web::Path<(ModId,)>,
|
||||
payload: Multipart,
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
|
||||
@@ -39,11 +40,14 @@ pub async fn version_create(
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
|
||||
let mod_id = url_data.into_inner().0.into();
|
||||
|
||||
let result = version_create_inner(
|
||||
payload,
|
||||
&mut transaction,
|
||||
&***file_host,
|
||||
&mut uploaded_files,
|
||||
mod_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -69,6 +73,7 @@ async fn version_create_inner(
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
file_host: &dyn FileHost,
|
||||
uploaded_files: &mut Vec<UploadedFile>,
|
||||
mod_id: models::ModId,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
|
||||
@@ -94,12 +99,9 @@ async fn version_create_inner(
|
||||
initial_version_data = Some(version_create_data);
|
||||
let version_create_data = initial_version_data.as_ref().unwrap();
|
||||
|
||||
// TODO: get mod_id from path (POST `/api/v1/mod/{mod_id}/version`)
|
||||
let mod_id: ModId = version_create_data.mod_id;
|
||||
|
||||
let results = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
|
||||
models::ModId::from(mod_id) as models::ModId
|
||||
mod_id as models::ModId
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
@@ -113,7 +115,7 @@ async fn version_create_inner(
|
||||
let results = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))",
|
||||
version_create_data.version_number,
|
||||
models::ModId::from(mod_id) as models::ModId,
|
||||
mod_id as models::ModId,
|
||||
)
|
||||
.fetch_one(&mut *transaction)
|
||||
.await?;
|
||||
@@ -125,7 +127,11 @@ async fn version_create_inner(
|
||||
}
|
||||
|
||||
let version_id: VersionId = models::generate_version_id(transaction).await?.into();
|
||||
let body_url = format!("data/{}/changelogs/{}/body.md", mod_id, version_id);
|
||||
let body_url = format!(
|
||||
"data/{}/changelogs/{}/body.md",
|
||||
ModId::from(mod_id),
|
||||
version_id
|
||||
);
|
||||
|
||||
let uploaded_text = file_host
|
||||
.upload_file(
|
||||
@@ -149,7 +155,7 @@ async fn version_create_inner(
|
||||
|
||||
version_builder = Some(VersionBuilder {
|
||||
version_id: version_id.into(),
|
||||
mod_id: mod_id.into(),
|
||||
mod_id,
|
||||
name: version_create_data.version_title.clone(),
|
||||
version_number: version_create_data.version_number.clone(),
|
||||
changelog_url: Some(format!("{}/{}", cdn_url, body_url)),
|
||||
@@ -246,16 +252,19 @@ async fn version_create_inner(
|
||||
hashes: file
|
||||
.hashes
|
||||
.iter()
|
||||
.map(|hash| crate::models::mods::FileHash {
|
||||
algorithm: hash.algorithm.clone(),
|
||||
// This is a hack since the hashes are currently stored as ASCII
|
||||
// in the database, but represented here as a Vec<u8>. At some
|
||||
// point we need to change the hash to be the real bytes in the
|
||||
// database and add more processing here.
|
||||
hash: String::from_utf8(hash.hash.clone()).unwrap(),
|
||||
.map(|hash| {
|
||||
(
|
||||
hash.algorithm.clone(),
|
||||
// This is a hack since the hashes are currently stored as ASCII
|
||||
// in the database, but represented here as a Vec<u8>. At some
|
||||
// point we need to change the hash to be the real bytes in the
|
||||
// database and add more processing here.
|
||||
String::from_utf8(hash.hash.clone()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
url: file.url.clone(),
|
||||
filename: file.filename.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
dependencies: version_data_safe.dependencies,
|
||||
@@ -270,9 +279,10 @@ async fn version_create_inner(
|
||||
|
||||
// TODO: file deletion, listing, etc
|
||||
|
||||
#[post("api/v1/version/{version_id}/file")]
|
||||
// under /api/v1/mod/{mod_id}/version/{version_id}
|
||||
#[post("file")]
|
||||
pub async fn upload_file_to_version(
|
||||
url_data: actix_web::web::Path<(VersionId,)>,
|
||||
url_data: actix_web::web::Path<(ModId, VersionId)>,
|
||||
payload: Multipart,
|
||||
client: Data<PgPool>,
|
||||
file_host: Data<std::sync::Arc<dyn FileHost + Send + Sync>>,
|
||||
@@ -280,7 +290,9 @@ pub async fn upload_file_to_version(
|
||||
let mut transaction = client.begin().await?;
|
||||
let mut uploaded_files = Vec::new();
|
||||
|
||||
let version_id = models::VersionId::from(url_data.into_inner().0);
|
||||
let data = url_data.into_inner();
|
||||
let mod_id = models::ModId::from(data.0);
|
||||
let version_id = models::VersionId::from(data.1);
|
||||
|
||||
let result = upload_file_to_version_inner(
|
||||
payload,
|
||||
@@ -288,6 +300,7 @@ pub async fn upload_file_to_version(
|
||||
&***file_host,
|
||||
&mut uploaded_files,
|
||||
version_id,
|
||||
mod_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -314,6 +327,7 @@ async fn upload_file_to_version_inner(
|
||||
file_host: &dyn FileHost,
|
||||
uploaded_files: &mut Vec<UploadedFile>,
|
||||
version_id: models::VersionId,
|
||||
mod_id: models::ModId,
|
||||
) -> Result<HttpResponse, CreateError> {
|
||||
let cdn_url = dotenv::var("CDN_URL")?;
|
||||
|
||||
@@ -339,6 +353,12 @@ async fn upload_file_to_version_inner(
|
||||
));
|
||||
}
|
||||
};
|
||||
if version.mod_id as u64 != mod_id.0 as u64 {
|
||||
return Err(CreateError::InvalidInput(
|
||||
"An invalid version id was supplied".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mod_id = ModId(version.mod_id as u64);
|
||||
let version_number = version.version_number;
|
||||
|
||||
|
||||
131
src/routes/versions.rs
Normal file
131
src/routes/versions.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use super::ApiError;
|
||||
use crate::database;
|
||||
use crate::models;
|
||||
use actix_web::{delete, get, web, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
// TODO: this needs filtering, and a better response type
|
||||
// Currently it only gives a list of ids, which have to be
|
||||
// requested manually. This route could give a list of the
|
||||
// ids as well as the supported versions and loaders, or
|
||||
// other info that is needed for selecting the right version.
|
||||
#[get("version")]
|
||||
pub async fn version_list(
|
||||
info: web::Path<(models::ids::ModId,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.0.into();
|
||||
|
||||
let mod_exists = sqlx::query!(
|
||||
"SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)",
|
||||
id as database::models::ModId,
|
||||
)
|
||||
.fetch_one(&**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?
|
||||
.exists;
|
||||
|
||||
if mod_exists.unwrap_or(false) {
|
||||
let mod_data = database::models::Version::get_mod_versions(id, &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
let response = mod_data
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.collect::<Vec<models::ids::VersionId>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("{version_id}")]
|
||||
pub async fn version_get(
|
||||
info: web::Path<(models::ids::ModId, models::ids::VersionId)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let id = info.1;
|
||||
let version_data = database::models::Version::get_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if let Some(data) = version_data {
|
||||
use models::mods::VersionType;
|
||||
|
||||
if models::ids::ModId::from(data.mod_id) != info.0 {
|
||||
// Version doesn't belong to that mod
|
||||
return Ok(HttpResponse::NotFound().body(""));
|
||||
}
|
||||
|
||||
let response = models::mods::Version {
|
||||
id: data.id.into(),
|
||||
mod_id: data.mod_id.into(),
|
||||
|
||||
name: data.name,
|
||||
version_number: data.version_number,
|
||||
changelog_url: data.changelog_url,
|
||||
date_published: data.date_published,
|
||||
downloads: data.downloads as u32,
|
||||
version_type: match data.release_channel.as_str() {
|
||||
"release" => VersionType::Release,
|
||||
"beta" => VersionType::Beta,
|
||||
"alpha" => VersionType::Alpha,
|
||||
_ => VersionType::Alpha,
|
||||
},
|
||||
|
||||
files: data
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
models::mods::VersionFile {
|
||||
url: f.url,
|
||||
filename: f.filename,
|
||||
// FIXME: Hashes are currently stored as an ascii byte slice instead
|
||||
// of as an actual byte array in the database
|
||||
hashes: f
|
||||
.hashes
|
||||
.into_iter()
|
||||
.map(|(k, v)| Some((k, String::from_utf8(v).ok()?)))
|
||||
.collect::<Option<_>>()
|
||||
.unwrap_or_else(Default::default),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
dependencies: Vec::new(), // TODO: dependencies
|
||||
game_versions: data
|
||||
.game_versions
|
||||
.into_iter()
|
||||
.map(models::mods::GameVersion)
|
||||
.collect(),
|
||||
loaders: data
|
||||
.loaders
|
||||
.into_iter()
|
||||
.map(models::mods::ModLoader)
|
||||
.collect(),
|
||||
};
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This really needs auth
|
||||
#[delete("{version_id}")]
|
||||
pub async fn version_delete(
|
||||
info: web::Path<(models::ids::ModId, models::ids::VersionId)>,
|
||||
pool: web::Data<PgPool>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
// TODO: check if the mod exists and matches the version id
|
||||
let id = info.1;
|
||||
let result = database::models::Version::remove_full(id.into(), &**pool)
|
||||
.await
|
||||
.map_err(|e| ApiError::DatabaseError(e.into()))?;
|
||||
|
||||
if result.is_some() {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body(""))
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ pub async fn index_curseforge(
|
||||
}
|
||||
}
|
||||
|
||||
if mod_categories.contains(&"fabric".to_owned()) {
|
||||
if mod_categories.iter().any(|e| e == "fabric") {
|
||||
using_fabric = true;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,8 @@ pub async fn index_curseforge(
|
||||
mod_categories.push(String::from("forge"));
|
||||
}
|
||||
if using_fabric {
|
||||
mod_categories.push(String::from("fabric"));
|
||||
// The only way this could happen is if "fabric" is already a category
|
||||
// mod_categories.push(String::from("fabric"));
|
||||
}
|
||||
|
||||
let mut mod_attachments = curseforge_mod.attachments;
|
||||
|
||||
@@ -34,7 +34,22 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let categories = sqlx::query!(
|
||||
// TODO: only loaders for recent versions? For mods that have moved from forge to fabric
|
||||
let loaders: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT loaders.loader FROM versions
|
||||
INNER JOIN loaders_versions lv ON lv.version_id = versions.id
|
||||
INNER JOIN loaders ON loaders.id = lv.loader_id
|
||||
WHERE versions.mod_id = $1
|
||||
",
|
||||
result.id
|
||||
)
|
||||
.fetch_many(&pool)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let mut categories = sqlx::query!(
|
||||
"
|
||||
SELECT c.category
|
||||
FROM mods_categories mc
|
||||
@@ -48,6 +63,8 @@ pub async fn index_local(pool: PgPool) -> Result<Vec<UploadSearchMod>, IndexingE
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
categories.extend(loaders);
|
||||
|
||||
let mut icon_url = "".to_string();
|
||||
|
||||
if let Some(url) = result.icon_url {
|
||||
|
||||
Reference in New Issue
Block a user