Compare commits
6 Commits
84fdf64793
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ef38898c9 | |||
| a323e5f2a1 | |||
| de97e4520c | |||
| d08c41e809 | |||
| cd25631863 | |||
| 0949b1452c |
@@ -1,2 +1,6 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = [
|
||||
"--codegen", "linker=clang",
|
||||
"--codegen", "link-arg=--ld-path=/usr/bin/mold",
|
||||
"--cfg", "tokio_unstable"
|
||||
]
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
*.sqlite-journal
|
||||
/pgdata
|
||||
|
||||
12
.sqlx/query-083f9f5bde8f4fe7fd9c88114f437c408eb84cbc06bd68bf2a17a6d6faa065ee.json
generated
Normal file
12
.sqlx/query-083f9f5bde8f4fe7fd9c88114f437c408eb84cbc06bd68bf2a17a6d6faa065ee.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO trips_items (\n item_id,\n trip_id,\n pick,\n pack,\n ready,\n new,\n user_id\n ) SELECT\n item_id,\n $1 as trip_id,\n pick,\n false as pack,\n false as ready,\n false as new,\n user_id\n FROM trips_items\n WHERE trip_id = $2 AND user_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "083f9f5bde8f4fe7fd9c88114f437c408eb84cbc06bd68bf2a17a6d6faa065ee"
|
||||
}
|
||||
12
.sqlx/query-0bcd2fcd725ba805a640c4066f5012a36c9f5a78dda51dc0643ebe8ea6028673.json
generated
Normal file
12
.sqlx/query-0bcd2fcd725ba805a640c4066f5012a36c9f5a78dda51dc0643ebe8ea6028673.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO trips_items (\n item_id,\n trip_id,\n pick,\n pack,\n ready,\n new,\n user_id\n ) SELECT\n id as item_id,\n $1 as trip_id,\n false as pick,\n false as pack,\n false as ready,\n false as new,\n user_id\n FROM inventory_items\n WHERE user_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0bcd2fcd725ba805a640c4066f5012a36c9f5a78dda51dc0643ebe8ea6028673"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE \n trips.id = $1\n AND todo.id = $2\n AND trips.user_id = $3\n ",
|
||||
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE\n trips.id = $1\n AND todo.id = $2\n AND trips.user_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -28,5 +28,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a9e75a36e019bb54ff06443a2ce98c788f88be295b6171f2f36e08c91109e380"
|
||||
"hash": "14e277298aa5ae032943a0d19e41d5fd444520c6555b2a55ba43c9547fc3b450"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO trips_items (\n item_id, \n trip_id, \n pick, \n pack, \n ready,\n new,\n user_id\n ) SELECT \n item_id,\n $1 as trip_id,\n pick,\n false as pack,\n false as ready,\n false as new,\n user_id\n FROM trips_items\n WHERE trip_id = $2 AND user_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "4017d92f0898c5046c4fbe1cd440ca73e5eb5d0794c679c9e5f05eb87d1defca"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE \n trips.id = $1\n AND trips.user_id = $2\n ",
|
||||
"query": "\n SELECT\n todo.id AS id,\n todo.description AS description,\n todo.done AS done\n FROM trip_todos AS todo\n INNER JOIN trips\n ON trips.id = todo.trip_id\n WHERE\n trips.id = $1\n AND trips.user_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -28,5 +28,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "17dfc8ae16d077ed71976012315376e1df403cb81ef173cb8811a5481186db7a"
|
||||
"hash": "451e4983d939cc2906192d7cefaf0634cc69f1f0460a75b740a0459b85ef9a4f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n id,\n name,\n weight,\n description,\n category_id\n FROM inventory_items\n WHERE \n category_id = ?\n AND user_id = ?",
|
||||
"query": "SELECT\n id,\n name,\n weight,\n description,\n category_id\n FROM inventory_items\n WHERE\n category_id = ?\n AND user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -40,5 +40,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2be306559191f0e14e04c33d42b7e5c2aca78e794465ead29273172204534e73"
|
||||
"hash": "61dc70362c2a16dc8ca370087a9e5e9dc030273c0747333b20d91c33d144df66"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n category.id as category_id,\n category.name as category_name,\n category.description AS category_description,\n inner.trip_id AS trip_id,\n inner.item_id AS item_id,\n inner.item_name AS item_name,\n inner.item_description AS item_description,\n inner.item_weight AS item_weight,\n inner.item_is_picked AS item_is_picked,\n inner.item_is_packed AS item_is_packed,\n inner.item_is_ready AS item_is_ready,\n inner.item_is_new AS item_is_new\n FROM inventory_items_categories AS category\n LEFT JOIN (\n SELECT\n trip.trip_id AS trip_id,\n category.id as category_id,\n category.name as category_name,\n category.description as category_description,\n item.id as item_id,\n item.name as item_name,\n item.description as item_description,\n item.weight as item_weight,\n trip.pick as item_is_picked,\n trip.pack as item_is_packed,\n trip.ready as item_is_ready,\n trip.new as item_is_new\n FROM trips_items as trip\n INNER JOIN inventory_items as item\n ON item.id = trip.item_id\n INNER JOIN inventory_items_categories as category\n ON category.id = item.category_id\n WHERE \n trip.trip_id = ?\n AND trip.user_id = ?\n ) AS inner\n ON inner.category_id = category.id\n WHERE category.id = ?\n ",
|
||||
"query": "\n SELECT\n category.id as category_id,\n category.name as category_name,\n category.description AS category_description,\n inner.trip_id AS trip_id,\n inner.item_id AS item_id,\n inner.item_name AS item_name,\n inner.item_description AS item_description,\n inner.item_weight AS item_weight,\n inner.item_is_picked AS item_is_picked,\n inner.item_is_packed AS item_is_packed,\n inner.item_is_ready AS item_is_ready,\n inner.item_is_new AS item_is_new\n FROM inventory_items_categories AS category\n LEFT JOIN (\n SELECT\n trip.trip_id AS trip_id,\n category.id as category_id,\n category.name as category_name,\n category.description as category_description,\n item.id as item_id,\n item.name as item_name,\n item.description as item_description,\n item.weight as item_weight,\n trip.pick as item_is_picked,\n trip.pack as item_is_packed,\n trip.ready as item_is_ready,\n trip.new as item_is_new\n FROM trips_items as trip\n INNER JOIN inventory_items as item\n ON item.id = trip.item_id\n INNER JOIN inventory_items_categories as category\n ON category.id = item.category_id\n WHERE\n trip.trip_id = ?\n AND trip.user_id = ?\n ) AS inner\n ON inner.category_id = category.id\n WHERE category.id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -82,5 +82,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4878a57b32697b1f18e3d2bf58d34b1ad5d05976eec3432406a3d8ddf92d8b94"
|
||||
"hash": "6e1a360cfba29dd5debdec0f18aa6ab2290acda5770fc2613fc57363fb285c90"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE trip_todos\n SET description = ?\n WHERE \n id = ? \n AND trip_id = ?\n AND EXISTS(SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)\n RETURNING\n id,\n description,\n done\n ",
|
||||
"query": "\n UPDATE trip_todos\n SET description = ?\n WHERE\n id = ?\n AND trip_id = ?\n AND EXISTS(SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)\n RETURNING\n id,\n description,\n done\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -28,5 +28,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4f9e7f676f42a548c8993a0182313fb3beebb547e658ea80302490953205a4c5"
|
||||
"hash": "724265c4373580cc4b4b1524f606171a239a50e324f3f693f2117c9f7919e28e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT COALESCE(SUM(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n INNER JOIN trips_items as t_item\n ON i_item.id = t_item.item_id\n WHERE \n category_id = ?\n AND category.user_id = ?\n AND t_item.pick = 1\n ",
|
||||
"query": "\n SELECT COALESCE(SUM(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n INNER JOIN trips_items as t_item\n ON i_item.id = t_item.item_id\n WHERE\n category_id = ?\n AND category.user_id = ?\n AND t_item.pick = 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,5 +16,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a65f6e3cfb2261d37ce3e806ec8d091314c0b206aee3bfe995d6295f4e0cc911"
|
||||
"hash": "821274890aeb47b59630936eceda8da5bdfc78e3e2b4454968cb8c653a286c33"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO trips_items (\n item_id, \n trip_id, \n pick, \n pack, \n ready,\n new,\n user_id\n ) SELECT \n id as item_id,\n $1 as trip_id,\n false as pick,\n false as pack,\n false as ready,\n false as new,\n user_id\n FROM inventory_items\n WHERE user_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "905a4518c657a01831fead855bad141d34f699c58b6aa5bee492b6eef2115d74"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n id,\n name,\n description\n FROM inventory_items_categories AS category\n WHERE \n category.id = ?\n AND category.user_id = ?",
|
||||
"query": "SELECT\n id,\n name,\n description\n FROM inventory_items_categories AS category\n WHERE\n category.id = ?\n AND category.user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -28,5 +28,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4118e44ba436f4ad7de970613fbaa3f88762d37b4e7dbd7b72a76298dad00fd6"
|
||||
"hash": "96d840777ed6454e1aa3892e52efd54141ce3f928fd85011ec9d0bac1a50b075"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id\n FROM inventory_items\n WHERE \n name = ?\n AND user_id = ?",
|
||||
"query": "SELECT id\n FROM inventory_items\n WHERE\n name = ?\n AND user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,5 +16,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c34c9c567a98937e043424db17246ddf23dbc8b618dca6c637f2d71d03841920"
|
||||
"hash": "b7632ae6bf28ad8f21a64ccbc4836dd29c2580da28d7edc8e46a15235f7d9c7d"
|
||||
}
|
||||
12
.sqlx/query-da3178af2beabc86c736b1faa572fb9db2bdc8206385c8802ca4d356fc7bee10.json
generated
Normal file
12
.sqlx/query-da3178af2beabc86c736b1faa572fb9db2bdc8206385c8802ca4d356fc7bee10.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM inventory_items\n WHERE\n id = ?\n AND user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "da3178af2beabc86c736b1faa572fb9db2bdc8206385c8802ca4d356fc7bee10"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n id,\n name,\n description \n FROM inventory_items_categories \n WHERE user_id = ?",
|
||||
"query": "SELECT\n id,\n name,\n description\n FROM inventory_items_categories\n WHERE user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -28,5 +28,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f361f939fd66d550cdfb2f75833d2d9ec2f56bb7170d173f834ac766e4d3026f"
|
||||
"hash": "e149344dd971add2b276de300795628e67ea6812ae383c2941bb29f7024eebe0"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n item.id AS id,\n item.name AS name,\n item.description AS description,\n weight,\n category.id AS category_id,\n category.name AS category_name,\n category.description AS category_description,\n product.id AS product_id,\n product.name AS product_name,\n product.description AS product_description,\n product.comment AS product_comment\n FROM inventory_items AS item\n INNER JOIN inventory_items_categories as category\n ON item.category_id = category.id\n LEFT JOIN inventory_products AS product\n ON item.product_id = product.id\n WHERE \n item.id = ?\n AND item.user_id = ?",
|
||||
"query": "SELECT\n item.id AS id,\n item.name AS name,\n item.description AS description,\n weight,\n category.id AS category_id,\n category.name AS category_name,\n category.description AS category_description,\n product.id AS product_id,\n product.name AS product_name,\n product.description AS product_description,\n product.comment AS product_comment\n FROM inventory_items AS item\n INNER JOIN inventory_items_categories as category\n ON item.category_id = category.id\n LEFT JOIN inventory_products AS product\n ON item.product_id = product.id\n WHERE\n item.id = ?\n AND item.user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -76,5 +76,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b4d2e5f57ee95dd449ab0d69450d471d9fd80b9fc943436185dcd25c7402271e"
|
||||
"hash": "e2ec3f4bf317aa12b185620209e7323e3f312ac695a811f4f5f81152f05362fa"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE inventory_items AS item\n SET\n name = ?,\n weight = ?\n WHERE \n item.id = ?\n AND item.user_id = ?\n RETURNING inventory_items.category_id AS id\n ",
|
||||
"query": "UPDATE inventory_items AS item\n SET\n name = ?,\n weight = ?\n WHERE\n item.id = ?\n AND item.user_id = ?\n RETURNING inventory_items.category_id AS id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,5 +16,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c1afeeec7f57f4af47d9a99f807b7be6fe2441af601a77d462a99ca7a22ca7c4"
|
||||
"hash": "e40a15294f141e89b9cac441a45ff1681d456350ba2e3e4133d369ca8ae773ba"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM inventory_items\n WHERE \n id = ?\n AND user_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ec968e7d97f30547cca37cb1834a054e1ba4ea5461e202cf329b929953c00894"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT COALESCE(MAX(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n WHERE \n category_id = ?\n AND category.user_id = ?\n ",
|
||||
"query": "\n SELECT COALESCE(MAX(i_item.weight), 0) as weight\n FROM inventory_items_categories as category\n INNER JOIN inventory_items as i_item\n ON i_item.category_id = category.id\n WHERE\n category_id = ?\n AND category.user_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -16,5 +16,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1899fd9182e8791104be2c369298b3f1731b34404ec5ac3d1312a8d84dbf5c7d"
|
||||
"hash": "f3f93de5727b17433b8a8076bb7172046eae708370c5afe7c4dd0a20518206e8"
|
||||
}
|
||||
108
Cargo.lock
generated
108
Cargo.lock
generated
@@ -47,47 +47,48 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.13"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.6"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.2"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -95,9 +96,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.82"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
@@ -143,9 +144,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
@@ -305,9 +306,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
@@ -359,9 +360,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.95"
|
||||
version = "1.0.97"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -411,9 +412,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "console-api"
|
||||
@@ -629,9 +630,9 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7"
|
||||
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -775,9 +776,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.14"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -842,9 +843,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
@@ -856,7 +857,7 @@ version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.3",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1100,7 +1101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1109,6 +1110,12 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -1144,9 +1151,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -1277,7 +1284,7 @@ checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.3",
|
||||
"hashbrown 0.14.5",
|
||||
"metrics",
|
||||
"num_cpus",
|
||||
"quanta",
|
||||
@@ -1370,9 +1377,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.44"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
@@ -1381,9 +1388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
@@ -1510,7 +1517,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.7.5",
|
||||
"axum-prometheus",
|
||||
"base64 0.22.0",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"console-subscriber",
|
||||
"futures",
|
||||
@@ -1780,9 +1787,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.0.1"
|
||||
version = "11.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1"
|
||||
checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
]
|
||||
@@ -1963,18 +1970,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.199"
|
||||
version = "1.0.200"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.199"
|
||||
version = "1.0.200"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2087,9 +2094,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@@ -2529,16 +2536,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.10"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3114,18 +3120,18 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
version = "0.7.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.32"
|
||||
version = "0.7.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -100,8 +100,8 @@ version = "0.7"
|
||||
#path = "./vendor/sqlx"
|
||||
features = [
|
||||
"runtime-tokio-rustls",
|
||||
# "offline",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"macros",
|
||||
"time",
|
||||
"migrate",
|
||||
|
||||
10
init-postgres.sh
Executable file
10
init-postgres.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o nounset
|
||||
|
||||
if [[ ! -e "./pgdata" ]] ; then
|
||||
initdb --locale=C.UTF-8 --encoding=UTF8 -D './pgdata' --user postgres
|
||||
mkdir ./pgdata/run
|
||||
fi
|
||||
|
||||
postgres -D ./pgdata -k run -h ""
|
||||
@@ -3,7 +3,7 @@ pub mod trips;
|
||||
pub mod crud {
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{models::Error, sqlite, Context};
|
||||
use crate::{db, models::Error, Context};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Create: Sized {
|
||||
@@ -15,7 +15,7 @@ pub mod crud {
|
||||
|
||||
async fn create(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
container: Self::Container,
|
||||
info: Self::Info,
|
||||
) -> Result<Self::Id, Error>;
|
||||
@@ -28,13 +28,13 @@ pub mod crud {
|
||||
|
||||
async fn findall(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
container: Self::Container,
|
||||
) -> Result<Vec<Self>, Error>;
|
||||
|
||||
async fn find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Self::Reference,
|
||||
) -> Result<Option<Self>, Error>;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub mod crud {
|
||||
|
||||
async fn update(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Self::Reference,
|
||||
update: Self::UpdateElement,
|
||||
) -> Result<Option<Self>, Error>;
|
||||
@@ -84,7 +84,7 @@ pub mod crud {
|
||||
|
||||
async fn delete_all<'c>(
|
||||
ctx: &Context,
|
||||
pool: &'c sqlite::Pool,
|
||||
pool: &'c db::Pool,
|
||||
container: Self::Container,
|
||||
ids: Vec<Self::Id>,
|
||||
) -> Result<bool, Error> {
|
||||
@@ -112,7 +112,7 @@ pub mod crud {
|
||||
|
||||
async fn set(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Self::Reference,
|
||||
value: bool,
|
||||
) -> Result<(), crate::Error>;
|
||||
|
||||
@@ -20,10 +20,10 @@ use crate::{
|
||||
route::{self, Toggle},
|
||||
view::{self, View},
|
||||
},
|
||||
htmx,
|
||||
db, htmx,
|
||||
models::{user::User, Error},
|
||||
routing::get_referer,
|
||||
sqlite, AppState, Context, RequestError,
|
||||
AppState, Context, RequestError,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
@@ -140,16 +140,16 @@ impl crud::Read for Todo {
|
||||
|
||||
async fn findall(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
container: Container,
|
||||
) -> Result<Vec<Self>, Error> {
|
||||
let trip_id_param = container.trip_id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
|
||||
let todos: Vec<Todo> = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Todo,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Todo,
|
||||
},
|
||||
pool,
|
||||
TodoRow,
|
||||
@@ -177,16 +177,16 @@ impl crud::Read for Todo {
|
||||
#[tracing::instrument]
|
||||
async fn find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Reference,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let trip_id_param = reference.container.trip_id.to_string();
|
||||
let todo_id_param = reference.id.0.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Todo,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Todo,
|
||||
},
|
||||
pool,
|
||||
TodoRow,
|
||||
@@ -228,7 +228,7 @@ impl crud::Create for Todo {
|
||||
|
||||
async fn create(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
container: Self::Container,
|
||||
info: Self::Info,
|
||||
) -> Result<Self::Id, Error> {
|
||||
@@ -238,9 +238,9 @@ impl crud::Create for Todo {
|
||||
let id_param = id.to_string();
|
||||
let trip_id_param = container.trip_id.to_string();
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Todo,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Todo,
|
||||
},
|
||||
pool,
|
||||
r#"
|
||||
@@ -305,7 +305,7 @@ impl crud::Update for Todo {
|
||||
#[tracing::instrument]
|
||||
async fn update(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Self::Reference,
|
||||
update_element: Self::UpdateElement,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
@@ -317,9 +317,9 @@ impl crud::Update for Todo {
|
||||
let done = state == State::Done.into();
|
||||
|
||||
let result = crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
TodoRow,
|
||||
@@ -351,9 +351,9 @@ impl crud::Update for Todo {
|
||||
let todo_id_param = reference.id.to_string();
|
||||
|
||||
let result = crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Todo,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Todo,
|
||||
},
|
||||
pool,
|
||||
TodoRow,
|
||||
@@ -400,9 +400,9 @@ impl crud::Delete for Todo {
|
||||
let trip_id_param = reference.container.trip_id.to_string();
|
||||
|
||||
let results = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Delete,
|
||||
component: sqlite::Component::Todo,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Delete,
|
||||
component: db::Component::Todo,
|
||||
},
|
||||
&mut *(db.acquire().await?),
|
||||
r#"
|
||||
@@ -945,7 +945,7 @@ impl crud::Toggle for StateUpdate {
|
||||
|
||||
async fn set(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
reference: Self::Reference,
|
||||
value: bool,
|
||||
) -> Result<(), crate::Error> {
|
||||
|
||||
152
src/db/error.rs
Normal file
152
src/db/error.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::fmt;
|
||||
|
||||
use sqlx::error::DatabaseError as _;
|
||||
|
||||
pub enum DatabaseError {
|
||||
/// Errors we can receive **from** the database that are caused by connection
|
||||
/// problems or schema problems (e.g. we get a return value that does not fit our enum,
|
||||
/// or a wrongly formatted date)
|
||||
Sql {
|
||||
description: String,
|
||||
},
|
||||
Uuid {
|
||||
description: String,
|
||||
},
|
||||
Enum {
|
||||
description: String,
|
||||
},
|
||||
TimeParse {
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Sql { description } => {
|
||||
write!(f, "SQL error: {description}")
|
||||
}
|
||||
Self::Uuid { description } => {
|
||||
write!(f, "UUID error: {description}")
|
||||
}
|
||||
Self::Enum { description } => {
|
||||
write!(f, "Enum error: {description}")
|
||||
}
|
||||
Self::TimeParse { description } => {
|
||||
write!(f, "Date parse error: {description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum QueryError {
|
||||
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
|
||||
/// inserts that violate unique constraints
|
||||
Duplicate {
|
||||
description: String,
|
||||
},
|
||||
NotFound {
|
||||
description: String,
|
||||
},
|
||||
ReferenceNotFound {
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for QueryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Duplicate { description } => {
|
||||
write!(f, "Duplicate data entry: {description}")
|
||||
}
|
||||
Self::NotFound { description } => {
|
||||
write!(f, "not found: {description}")
|
||||
}
|
||||
Self::ReferenceNotFound { description } => {
|
||||
write!(f, "SQL foreign key reference was not found: {description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Error {
|
||||
Database(DatabaseError),
|
||||
Query(QueryError),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Database(error) => write!(f, "{error}"),
|
||||
Self::Query(error) => write!(f, "{error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// defer to Display
|
||||
write!(f, "SQL error: {self}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for Error {
|
||||
fn from(value: uuid::Error) -> Self {
|
||||
Error::Database(DatabaseError::Uuid {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::error::Format> for Error {
|
||||
fn from(value: time::error::Format) -> Self {
|
||||
Error::Database(DatabaseError::TimeParse {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
match value {
|
||||
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
|
||||
description: value.to_string(),
|
||||
}),
|
||||
sqlx::Error::Database(ref error) => {
|
||||
let sqlite_error = error.downcast_ref::<sqlx::sqlite::SqliteError>();
|
||||
if let Some(code) = sqlite_error.code() {
|
||||
match &*code {
|
||||
// SQLITE_CONSTRAINT_FOREIGNKEY
|
||||
"787" => Error::Query(QueryError::ReferenceNotFound {
|
||||
description: "foreign key reference not found".to_string(),
|
||||
}),
|
||||
// SQLITE_CONSTRAINT_UNIQUE
|
||||
"2067" => Error::Query(QueryError::Duplicate {
|
||||
description: "item with unique constraint already exists".to_string(),
|
||||
}),
|
||||
_ => Error::Database(DatabaseError::Sql {
|
||||
description: format!("got error with unknown code: {sqlite_error}"),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Error::Database(DatabaseError::Sql {
|
||||
description: format!("got error without code: {sqlite_error}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Error::Database(DatabaseError::Sql {
|
||||
description: format!("got unknown error: {value}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::error::Parse> for Error {
|
||||
fn from(value: time::error::Parse) -> Self {
|
||||
Error::Database(DatabaseError::TimeParse {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
@@ -1,68 +1,13 @@
|
||||
use std::fmt;
|
||||
use std::time;
|
||||
|
||||
use base64::Engine as _;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::Instrument;
|
||||
use std::fmt;
|
||||
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use sqlx::ConnectOptions;
|
||||
pub use sqlx::{Pool as SqlitePool, Sqlite};
|
||||
|
||||
use std::str::FromStr as _;
|
||||
|
||||
pub use sqlx::Type;
|
||||
|
||||
use crate::StartError;
|
||||
pub mod error;
|
||||
pub mod postgres;
|
||||
pub mod sqlite;
|
||||
|
||||
pub type Pool = sqlx::Pool<sqlx::Sqlite>;
|
||||
|
||||
pub fn int_to_bool(value: i32) -> bool {
|
||||
match value {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => panic!("got invalid boolean from sqlite"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn init_database_pool(url: &str) -> Result<Pool, StartError> {
|
||||
async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(
|
||||
SqliteConnectOptions::from_str(url)?
|
||||
.log_statements(log::LevelFilter::Debug)
|
||||
.log_slow_statements(log::LevelFilter::Warn, time::Duration::from_millis(100))
|
||||
.pragma("foreign_keys", "1"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
.instrument(tracing::info_span!("packager::sql::pool"))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn migrate(url: &str) -> Result<(), StartError> {
|
||||
async {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(
|
||||
SqliteConnectOptions::from_str(url)?
|
||||
.pragma("foreign_keys", "0")
|
||||
.log_statements(log::LevelFilter::Debug),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::migrate!().run(&pool).await
|
||||
}
|
||||
.instrument(tracing::info_span!("packager::sql::migrate"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub enum QueryType {
|
||||
Insert,
|
||||
Update,
|
||||
@@ -171,7 +116,7 @@ macro_rules! query_all {
|
||||
use tracing::Instrument as _;
|
||||
use futures::TryStreamExt as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: Result<Vec<$struct_into>, Error> = sqlx::query_as!(
|
||||
$struct_row,
|
||||
$query,
|
||||
@@ -197,7 +142,7 @@ macro_rules! query_one {
|
||||
{
|
||||
use tracing::Instrument as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: Result<Option<$struct_into>, Error> = sqlx::query_as!(
|
||||
$struct_row,
|
||||
$query,
|
||||
@@ -221,7 +166,7 @@ macro_rules! query_exists {
|
||||
{
|
||||
use tracing::Instrument as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: bool = sqlx::query!(
|
||||
$query,
|
||||
$( $args )*
|
||||
@@ -250,7 +195,7 @@ macro_rules! execute {
|
||||
{
|
||||
use tracing::Instrument as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: Result<sqlx::sqlite::SqliteQueryResult, Error> = sqlx::query!(
|
||||
$query,
|
||||
$( $args )*
|
||||
@@ -298,7 +243,7 @@ macro_rules! execute_returning {
|
||||
use tracing::Instrument as _;
|
||||
use futures::TryFutureExt as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: Result<$t, Error> = sqlx::query!(
|
||||
$query,
|
||||
$( $args )*
|
||||
@@ -323,7 +268,7 @@ macro_rules! execute_returning_uuid {
|
||||
use tracing::Instrument as _;
|
||||
use futures::TryFutureExt as _;
|
||||
async {
|
||||
$crate::sqlite::sqlx_query($class, $query, &[]);
|
||||
$crate::db::sqlx_query($class, $query, &[]);
|
||||
let result: Result<Uuid, Error> = sqlx::query!(
|
||||
$query,
|
||||
$( $args )*
|
||||
1
src/db/postgres.rs
Normal file
1
src/db/postgres.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
59
src/db/sqlite.rs
Normal file
59
src/db/sqlite.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use std::time;
|
||||
|
||||
use tracing::Instrument;
|
||||
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
|
||||
use sqlx::ConnectOptions;
|
||||
pub use sqlx::{Pool as SqlitePool, Sqlite};
|
||||
|
||||
use std::str::FromStr as _;
|
||||
|
||||
pub use sqlx::Type;
|
||||
|
||||
use crate::StartError;
|
||||
|
||||
pub fn int_to_bool(value: i32) -> bool {
|
||||
match value {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => panic!("got invalid boolean from sqlite"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn init_database_pool(url: &str) -> Result<sqlx::Pool<sqlx::Sqlite>, StartError> {
|
||||
async {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(
|
||||
SqliteConnectOptions::from_str(url)?
|
||||
.log_statements(log::LevelFilter::Debug)
|
||||
.log_slow_statements(log::LevelFilter::Warn, time::Duration::from_millis(100))
|
||||
.pragma("foreign_keys", "1"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
.instrument(tracing::info_span!("packager::sql::pool"))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn migrate(url: &str) -> Result<(), StartError> {
|
||||
async {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(
|
||||
SqliteConnectOptions::from_str(url)?
|
||||
.pragma("foreign_keys", "0")
|
||||
.log_statements(log::LevelFilter::Debug),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::migrate!().run(&pool).await
|
||||
}
|
||||
.instrument(tracing::info_span!("packager::sql::migrate"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
282
src/elements/list.rs
Normal file
282
src/elements/list.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use maud::{html, Markup};
|
||||
|
||||
use super::HxConfig;
|
||||
|
||||
pub struct Link<'a> {
|
||||
pub text: &'a str,
|
||||
pub href: String,
|
||||
pub hx_config: Option<HxConfig>,
|
||||
}
|
||||
|
||||
pub struct NumberWithBar {
|
||||
pub value: i64,
|
||||
pub max_value: i64,
|
||||
}
|
||||
|
||||
pub enum CellType<'a> {
|
||||
Text(&'a str),
|
||||
Link(Link<'a>),
|
||||
NumberWithBar(NumberWithBar),
|
||||
}
|
||||
|
||||
pub struct Cell<'a> {
|
||||
pub cell_type: CellType<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Cell<'a> {
|
||||
fn render(self, _is_edit: bool) -> Markup {
|
||||
match self.cell_type {
|
||||
CellType::Text(text) => html!(
|
||||
td
|
||||
."border"
|
||||
."p-0"
|
||||
."m-0"
|
||||
{
|
||||
p { (text) }
|
||||
}
|
||||
),
|
||||
CellType::Link(link) => {
|
||||
let (hx_post, hx_swap, hx_target) = if let Some(hx_config) = link.hx_config {
|
||||
(
|
||||
Some(hx_config.hx_post),
|
||||
Some(hx_config.hx_swap),
|
||||
Some(hx_config.hx_target),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
html!(
|
||||
td
|
||||
."border"
|
||||
."p-0"
|
||||
."m-0"
|
||||
{
|
||||
a
|
||||
."inline-block"
|
||||
."p-2"
|
||||
."m-0"
|
||||
."w-full"
|
||||
|
||||
href=(link.href)
|
||||
hx-post=[hx_post]
|
||||
hx-swap=[hx_swap]
|
||||
hx-target=[hx_target]
|
||||
{
|
||||
(link.text)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
CellType::NumberWithBar(number) => html!(
|
||||
td
|
||||
."border"
|
||||
."p-2"
|
||||
."m-0"
|
||||
style="position:relative;"
|
||||
{
|
||||
p {
|
||||
(number.value)
|
||||
}
|
||||
div ."bg-blue-600" ."h-1.5"
|
||||
style=(
|
||||
format!(
|
||||
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
|
||||
width=(
|
||||
(number.value as f64)
|
||||
/ (number.max_value as f64)
|
||||
* 100.0
|
||||
)
|
||||
)
|
||||
)
|
||||
{}
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Button {
|
||||
pub icon: super::Icon,
|
||||
pub action: Action,
|
||||
pub hx_config: Option<HxConfig>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
fn render(self) -> Markup {
|
||||
html!(
|
||||
td
|
||||
."border-none"
|
||||
."p-0"
|
||||
.(self.icon.background())
|
||||
.(self.icon.background_hover())
|
||||
."h-full"
|
||||
."w-10"
|
||||
{
|
||||
@match self.action {
|
||||
Action::Href(href) => {
|
||||
a
|
||||
href=(href)
|
||||
."aspect-square"
|
||||
."flex"
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
."text-xl"
|
||||
.(self.icon.mdi_class())
|
||||
{}
|
||||
}
|
||||
}
|
||||
Action::Submit(form) => {
|
||||
button
|
||||
."aspect-square"
|
||||
."flex"
|
||||
."w-full"
|
||||
."h-full"
|
||||
type="submit"
|
||||
form=(form)
|
||||
{
|
||||
span
|
||||
."m-auto"
|
||||
."mdi"
|
||||
.(self.icon.mdi_class())
|
||||
."text-xl"
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Action {
|
||||
Href(String),
|
||||
Submit(&'static str),
|
||||
}
|
||||
|
||||
pub struct EditingConfig {
|
||||
pub edit_action: Action,
|
||||
pub edit_hx_config: Option<HxConfig>,
|
||||
pub delete_action: Action,
|
||||
pub delete_hx_config: Option<HxConfig>,
|
||||
pub save_action: Action,
|
||||
pub save_hx_config: Option<HxConfig>,
|
||||
pub cancel_action: Action,
|
||||
pub cancel_hx_config: Option<HxConfig>,
|
||||
}
|
||||
|
||||
pub trait Row {
|
||||
fn is_active(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_edit(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn cells(&self) -> Vec<Cell>;
|
||||
}
|
||||
|
||||
pub struct Header<'c> {
|
||||
pub cells: Vec<Option<HeaderCell<'c>>>,
|
||||
}
|
||||
|
||||
pub struct HeaderCell<'a> {
|
||||
pub title: &'a str,
|
||||
}
|
||||
|
||||
impl<'c> HeaderCell<'c> {
|
||||
fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
}
|
||||
|
||||
pub struct List<'hc, R>
|
||||
where
|
||||
R: Row,
|
||||
{
|
||||
pub id: Option<&'static str>,
|
||||
pub header: Header<'hc>,
|
||||
pub rows: Vec<R>,
|
||||
pub editing_config: Option<Box<dyn Fn(R) -> EditingConfig>>,
|
||||
}
|
||||
|
||||
impl<'hc, R> List<'hc, R>
|
||||
where
|
||||
R: Row,
|
||||
{
|
||||
pub fn render(self) -> Markup {
|
||||
html!(
|
||||
table
|
||||
id=[self.id]
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
thead ."bg-gray-200" {
|
||||
tr
|
||||
."h-10"
|
||||
{
|
||||
@for header_cell in self.header.cells.iter() {
|
||||
th ."border" ."p-2" { (header_cell.as_ref().map_or("", |c| c.title())) }
|
||||
}
|
||||
@if self.editing_config.is_some() {
|
||||
th {}
|
||||
th {}
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for row in self.rows.into_iter() {
|
||||
@let active = row.is_active();
|
||||
@let is_edit = row.is_edit();
|
||||
tr
|
||||
."h-10"
|
||||
."hover:bg-gray-100"
|
||||
."outline"[active]
|
||||
."outline-2"[active]
|
||||
."outline-indigo-300"[active]
|
||||
."pointer-events-none"[active]
|
||||
."font-bold"[active]
|
||||
{
|
||||
@for cell in row.cells() {
|
||||
(cell.render(is_edit))
|
||||
}
|
||||
@if let Some(ref edit_config) = self.editing_config {
|
||||
@let edit_config = (*edit_config)(row);
|
||||
@if is_edit {
|
||||
(Button {
|
||||
icon: super::Icon::Save,
|
||||
action: edit_config.save_action,
|
||||
hx_config: edit_config.save_hx_config,
|
||||
}.render())
|
||||
(Button {
|
||||
icon: super::Icon::Cancel,
|
||||
action: edit_config.cancel_action,
|
||||
hx_config: edit_config.cancel_hx_config,
|
||||
}.render())
|
||||
} @else {
|
||||
(Button {
|
||||
icon: super::Icon::Edit,
|
||||
action: edit_config.edit_action,
|
||||
hx_config: edit_config.edit_hx_config,
|
||||
} .render())
|
||||
(Button {
|
||||
icon: super::Icon::Delete,
|
||||
action: edit_config.delete_action,
|
||||
hx_config: edit_config.delete_hx_config,
|
||||
}.render())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
61
src/elements/mod.rs
Normal file
61
src/elements/mod.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
pub mod list;
|
||||
|
||||
pub enum HxSwap {
|
||||
OuterHtml,
|
||||
}
|
||||
|
||||
pub enum Icon {
|
||||
Edit,
|
||||
Delete,
|
||||
Save,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn mdi_class(&self) -> &'static str {
|
||||
match self {
|
||||
Icon::Edit => "mdi-pencil",
|
||||
Icon::Delete => "mdi-delete",
|
||||
Icon::Save => "mdi-content-save",
|
||||
Icon::Cancel => "mdi-cancel",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background(&self) -> &'static str {
|
||||
match self {
|
||||
Icon::Edit => "bg-blue-200",
|
||||
Icon::Delete => "bg-red-200",
|
||||
Icon::Save => "bg-green-100",
|
||||
Icon::Cancel => "bg-red-100",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn background_hover(&self) -> &'static str {
|
||||
match self {
|
||||
Icon::Edit => "hover:bg-blue-400",
|
||||
Icon::Delete => "hover:bg-red-400",
|
||||
Icon::Save => "hover:bg-green-200",
|
||||
Icon::Cancel => "hover:bg-red-200",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HxSwap {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
HxSwap::OuterHtml => "outerHtml",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HxConfig {
|
||||
pub hx_post: String,
|
||||
pub hx_swap: HxSwap,
|
||||
pub hx_target: &'static str,
|
||||
}
|
||||
@@ -5,11 +5,12 @@ use std::fmt;
|
||||
pub mod auth;
|
||||
pub mod cli;
|
||||
pub mod components;
|
||||
pub mod db;
|
||||
pub mod elements;
|
||||
pub mod error;
|
||||
pub mod htmx;
|
||||
pub mod models;
|
||||
pub mod routing;
|
||||
pub mod sqlite;
|
||||
pub mod telemetry;
|
||||
|
||||
mod view;
|
||||
@@ -18,7 +19,7 @@ pub use error::{AuthError, CommandError, Error, RequestError, StartError};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
pub database_pool: sqlite::Pool,
|
||||
pub database_pool: db::Pool,
|
||||
pub client_state: ClientState,
|
||||
pub auth_config: auth::Config,
|
||||
}
|
||||
|
||||
15
src/main.rs
15
src/main.rs
@@ -4,7 +4,7 @@ use std::process::ExitCode;
|
||||
use std::str::FromStr;
|
||||
|
||||
use packager::{
|
||||
auth, cli, models, routing, sqlite, telemetry, AppState, ClientState, Error, StartError,
|
||||
auth, cli, db, models, routing, telemetry, AppState, ClientState, Error, StartError,
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
@@ -59,12 +59,12 @@ async fn main() -> MainResult {
|
||||
Box::pin(async move {
|
||||
match args.command {
|
||||
cli::Command::Serve(serve_args) => {
|
||||
if let Err(e) = sqlite::migrate(&args.database_url).await {
|
||||
if let Err(e) = db::sqlite::migrate(&args.database_url).await {
|
||||
return <_ as Into<Error>>::into(e).into();
|
||||
}
|
||||
|
||||
let database_pool =
|
||||
match sqlite::init_database_pool(&args.database_url).await {
|
||||
match db::sqlite::init_database_pool(&args.database_url).await {
|
||||
Ok(pool) => pool,
|
||||
Err(e) => return <_ as Into<Error>>::into(e).into(),
|
||||
};
|
||||
@@ -152,8 +152,11 @@ async fn main() -> MainResult {
|
||||
cli::Command::Admin(admin_command) => match admin_command {
|
||||
cli::Admin::User(cmd) => match cmd {
|
||||
cli::UserCommand::Create(user) => {
|
||||
let database_pool =
|
||||
match sqlite::init_database_pool(&args.database_url).await {
|
||||
let database_pool = match db::sqlite::init_database_pool(
|
||||
&args.database_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(pool) => pool,
|
||||
Err(e) => return <_ as Into<Error>>::into(e).into(),
|
||||
};
|
||||
@@ -188,7 +191,7 @@ async fn main() -> MainResult {
|
||||
},
|
||||
},
|
||||
cli::Command::Migrate => {
|
||||
if let Err(e) = sqlite::migrate(&args.database_url).await {
|
||||
if let Err(e) = db::sqlite::migrate(&args.database_url).await {
|
||||
return <_ as Into<Error>>::into(e).into();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,152 +1 @@
|
||||
use std::fmt;
|
||||
|
||||
use sqlx::error::DatabaseError as _;
|
||||
|
||||
pub enum DatabaseError {
|
||||
/// Errors we can receive **from** the database that are caused by connection
|
||||
/// problems or schema problems (e.g. we get a return value that does not fit our enum,
|
||||
/// or a wrongly formatted date)
|
||||
Sql {
|
||||
description: String,
|
||||
},
|
||||
Uuid {
|
||||
description: String,
|
||||
},
|
||||
Enum {
|
||||
description: String,
|
||||
},
|
||||
TimeParse {
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Sql { description } => {
|
||||
write!(f, "SQL error: {description}")
|
||||
}
|
||||
Self::Uuid { description } => {
|
||||
write!(f, "UUID error: {description}")
|
||||
}
|
||||
Self::Enum { description } => {
|
||||
write!(f, "Enum error: {description}")
|
||||
}
|
||||
Self::TimeParse { description } => {
|
||||
write!(f, "Date parse error: {description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum QueryError {
|
||||
/// Errors that are caused by wrong input data, e.g. ids that cannot be found, or
|
||||
/// inserts that violate unique constraints
|
||||
Duplicate {
|
||||
description: String,
|
||||
},
|
||||
NotFound {
|
||||
description: String,
|
||||
},
|
||||
ReferenceNotFound {
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for QueryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Duplicate { description } => {
|
||||
write!(f, "Duplicate data entry: {description}")
|
||||
}
|
||||
Self::NotFound { description } => {
|
||||
write!(f, "not found: {description}")
|
||||
}
|
||||
Self::ReferenceNotFound { description } => {
|
||||
write!(f, "SQL foreign key reference was not found: {description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Error {
|
||||
Database(DatabaseError),
|
||||
Query(QueryError),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Database(error) => write!(f, "{error}"),
|
||||
Self::Query(error) => write!(f, "{error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// defer to Display
|
||||
write!(f, "SQL error: {self}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for Error {
|
||||
fn from(value: uuid::Error) -> Self {
|
||||
Error::Database(DatabaseError::Uuid {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::error::Format> for Error {
|
||||
fn from(value: time::error::Format) -> Self {
|
||||
Error::Database(DatabaseError::TimeParse {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(value: sqlx::Error) -> Self {
|
||||
match value {
|
||||
sqlx::Error::RowNotFound => Error::Query(QueryError::NotFound {
|
||||
description: value.to_string(),
|
||||
}),
|
||||
sqlx::Error::Database(ref error) => {
|
||||
let sqlite_error = error.downcast_ref::<sqlx::sqlite::SqliteError>();
|
||||
if let Some(code) = sqlite_error.code() {
|
||||
match &*code {
|
||||
// SQLITE_CONSTRAINT_FOREIGNKEY
|
||||
"787" => Error::Query(QueryError::ReferenceNotFound {
|
||||
description: "foreign key reference not found".to_string(),
|
||||
}),
|
||||
// SQLITE_CONSTRAINT_UNIQUE
|
||||
"2067" => Error::Query(QueryError::Duplicate {
|
||||
description: "item with unique constraint already exists".to_string(),
|
||||
}),
|
||||
_ => Error::Database(DatabaseError::Sql {
|
||||
description: format!("got error with unknown code: {sqlite_error}"),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Error::Database(DatabaseError::Sql {
|
||||
description: format!("got error without code: {sqlite_error}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Error::Database(DatabaseError::Sql {
|
||||
description: format!("got unknown error: {value}"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::error::Parse> for Error {
|
||||
fn from(value: time::error::Parse) -> Self {
|
||||
Error::Database(DatabaseError::TimeParse {
|
||||
description: value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
pub use crate::db::error::{Error, QueryError, DatabaseError};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::Error;
|
||||
use crate::{sqlite, Context};
|
||||
use crate::{db, Context};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -9,13 +9,13 @@ pub struct Inventory {
|
||||
|
||||
impl Inventory {
|
||||
#[tracing::instrument]
|
||||
pub async fn load(ctx: &Context, pool: &sqlite::Pool) -> Result<Self, Error> {
|
||||
pub async fn load(ctx: &Context, pool: &db::Pool) -> Result<Self, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
|
||||
let mut categories = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
DbCategoryRow,
|
||||
@@ -69,15 +69,15 @@ impl Category {
|
||||
#[tracing::instrument]
|
||||
pub async fn _find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
) -> Result<Option<Category>, Error> {
|
||||
let id_param = id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
DbCategoryRow,
|
||||
@@ -97,14 +97,14 @@ impl Category {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn save(ctx: &Context, pool: &sqlite::Pool, name: &str) -> Result<Uuid, Error> {
|
||||
pub async fn save(ctx: &Context, pool: &db::Pool, name: &str) -> Result<Uuid, Error> {
|
||||
let id = Uuid::new_v4();
|
||||
let id_param = id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"INSERT INTO inventory_items_categories
|
||||
@@ -136,14 +136,14 @@ impl Category {
|
||||
pub async fn populate_items(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
) -> Result<(), Error> {
|
||||
let id = self.id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let items = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
DbInventoryItemsRow,
|
||||
@@ -232,14 +232,18 @@ impl TryFrom<DbInventoryItemRow> for InventoryItem {
|
||||
|
||||
impl InventoryItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn find(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result<Option<Self>, Error> {
|
||||
pub async fn find(
|
||||
ctx: &Context,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let id_param = id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
DbInventoryItemRow,
|
||||
@@ -273,14 +277,14 @@ impl InventoryItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn name_exists(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
name: &str,
|
||||
) -> Result<bool, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::query_exists!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"SELECT id
|
||||
@@ -295,13 +299,13 @@ impl InventoryItem {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete(ctx: &Context, pool: &sqlite::Pool, id: Uuid) -> Result<bool, Error> {
|
||||
pub async fn delete(ctx: &Context, pool: &db::Pool, id: Uuid) -> Result<bool, Error> {
|
||||
let id_param = id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let results = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Delete,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Delete,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"DELETE FROM inventory_items
|
||||
@@ -319,7 +323,7 @@ impl InventoryItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn update(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
name: &str,
|
||||
weight: u32,
|
||||
@@ -329,9 +333,9 @@ impl InventoryItem {
|
||||
|
||||
let id_param = id.to_string();
|
||||
crate::execute_returning_uuid!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"UPDATE inventory_items AS item
|
||||
@@ -354,7 +358,7 @@ impl InventoryItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn save(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
name: &str,
|
||||
category_id: Uuid,
|
||||
weight: u32,
|
||||
@@ -365,9 +369,9 @@ impl InventoryItem {
|
||||
let category_id_param = category_id.to_string();
|
||||
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"INSERT INTO inventory_items
|
||||
@@ -389,15 +393,15 @@ impl InventoryItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn get_category_max_weight(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
category_id: Uuid,
|
||||
) -> Result<i64, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let category_id_param = category_id.to_string();
|
||||
let weight = crate::execute_returning!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"
|
||||
@@ -455,15 +459,15 @@ impl Item {
|
||||
#[tracing::instrument]
|
||||
pub async fn _get_category_total_picked_weight(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
category_id: Uuid,
|
||||
) -> Result<i64, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let category_id_param = category_id.to_string();
|
||||
crate::execute_returning!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Inventory,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Inventory,
|
||||
},
|
||||
pool,
|
||||
"
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::{
|
||||
inventory,
|
||||
};
|
||||
|
||||
use crate::{sqlite, Context};
|
||||
use crate::{db, Context};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time;
|
||||
@@ -38,7 +38,7 @@ use uuid::Uuid;
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(sqlite::Type, PartialEq, PartialOrd, Deserialize, Debug)]
|
||||
#[derive(db::sqlite::Type, PartialEq, PartialOrd, Deserialize, Debug)]
|
||||
pub enum TripState {
|
||||
Init,
|
||||
Planning,
|
||||
@@ -156,7 +156,7 @@ impl TripCategory {
|
||||
#[tracing::instrument]
|
||||
pub async fn find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
category_id: Uuid,
|
||||
) -> Result<Option<TripCategory>, Error> {
|
||||
@@ -222,9 +222,9 @@ impl TripCategory {
|
||||
}
|
||||
|
||||
let mut rows = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
Row,
|
||||
@@ -346,7 +346,7 @@ impl TripItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
item_id: Uuid,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
@@ -354,9 +354,9 @@ impl TripItem {
|
||||
let item_id_param = item_id.to_string();
|
||||
let trip_id_param = trip_id.to_string();
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
DbTripsItemsRow,
|
||||
@@ -389,7 +389,7 @@ impl TripItem {
|
||||
#[tracing::instrument]
|
||||
pub async fn set_state(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
item_id: Uuid,
|
||||
key: TripItemStateKey,
|
||||
@@ -401,9 +401,9 @@ impl TripItem {
|
||||
let result = match key {
|
||||
TripItemStateKey::Pick => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips_items
|
||||
@@ -420,9 +420,9 @@ impl TripItem {
|
||||
}
|
||||
TripItemStateKey::Pack => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips_items
|
||||
@@ -439,9 +439,9 @@ impl TripItem {
|
||||
}
|
||||
TripItemStateKey::Ready => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips_items
|
||||
@@ -533,12 +533,12 @@ pub enum TripAttribute {
|
||||
|
||||
impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result<Vec<Trip>, Error> {
|
||||
pub async fn all(ctx: &Context, pool: &db::Pool) -> Result<Vec<Trip>, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let mut trips = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
DbTripRow,
|
||||
@@ -566,15 +566,15 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn find(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let trip_id_param = trip_id.to_string();
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
DbTripRow,
|
||||
@@ -600,7 +600,7 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn trip_type_remove(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
type_id: Uuid,
|
||||
) -> Result<bool, Error> {
|
||||
@@ -609,9 +609,9 @@ impl Trip {
|
||||
let type_id_param = type_id.to_string();
|
||||
|
||||
let results = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Delete,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Delete,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"DELETE FROM trips_to_trips_types AS ttt
|
||||
@@ -635,7 +635,7 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn trip_type_add(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
type_id: Uuid,
|
||||
) -> Result<(), Error> {
|
||||
@@ -645,9 +645,9 @@ impl Trip {
|
||||
let type_id_param = type_id.to_string();
|
||||
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"INSERT INTO
|
||||
@@ -673,16 +673,16 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn set_state(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
new_state: &TripState,
|
||||
) -> Result<bool, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let trip_id_param = id.to_string();
|
||||
let result = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -700,16 +700,16 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn set_comment(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
new_comment: &str,
|
||||
) -> Result<bool, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let trip_id_param = id.to_string();
|
||||
let result = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -727,7 +727,7 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn set_attribute(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
attribute: TripAttribute,
|
||||
value: &str,
|
||||
@@ -737,9 +737,9 @@ impl Trip {
|
||||
let result = match attribute {
|
||||
TripAttribute::Name => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -754,9 +754,9 @@ impl Trip {
|
||||
|
||||
TripAttribute::DateStart => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -770,9 +770,9 @@ impl Trip {
|
||||
}
|
||||
TripAttribute::DateEnd => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -786,9 +786,9 @@ impl Trip {
|
||||
}
|
||||
TripAttribute::Location => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -802,9 +802,9 @@ impl Trip {
|
||||
}
|
||||
TripAttribute::TempMin => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -818,9 +818,9 @@ impl Trip {
|
||||
}
|
||||
TripAttribute::TempMax => {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips
|
||||
@@ -844,7 +844,7 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn save(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
name: &str,
|
||||
date_start: time::Date,
|
||||
date_end: time::Date,
|
||||
@@ -861,9 +861,9 @@ impl Trip {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
&mut *transaction,
|
||||
"INSERT INTO trips
|
||||
@@ -882,9 +882,9 @@ impl Trip {
|
||||
if let Some(copy_from_trip_id) = copy_from {
|
||||
let copy_from_trip_id_param = copy_from_trip_id.to_string();
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
&mut *transaction,
|
||||
r#"INSERT INTO trips_items (
|
||||
@@ -912,9 +912,9 @@ impl Trip {
|
||||
.await?;
|
||||
} else {
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
&mut *transaction,
|
||||
r#"INSERT INTO trips_items (
|
||||
@@ -949,15 +949,15 @@ impl Trip {
|
||||
#[tracing::instrument]
|
||||
pub async fn find_total_picked_weight(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
trip_id: Uuid,
|
||||
) -> Result<i64, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let trip_id_param = trip_id.to_string();
|
||||
let weight = crate::execute_returning!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"
|
||||
@@ -1018,7 +1018,7 @@ impl Trip {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn load_todos(&mut self, ctx: &Context, pool: &sqlite::Pool) -> Result<(), Error> {
|
||||
pub async fn load_todos(&mut self, ctx: &Context, pool: &db::Pool) -> Result<(), Error> {
|
||||
self.todos = Some(
|
||||
crate::components::trips::todos::Todo::findall(
|
||||
ctx,
|
||||
@@ -1031,17 +1031,13 @@ impl Trip {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn load_trips_types(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn load_trips_types(&mut self, ctx: &Context, pool: &db::Pool) -> Result<(), Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let id = self.id.to_string();
|
||||
let types = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
TripTypeRow,
|
||||
@@ -1078,7 +1074,7 @@ impl Trip {
|
||||
pub async fn sync_trip_items_with_inventory(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
) -> Result<(), Error> {
|
||||
// we need to get all items that are part of the inventory but not
|
||||
// part of the trip items
|
||||
@@ -1107,9 +1103,9 @@ impl Trip {
|
||||
}
|
||||
|
||||
let unsynced_items: Vec<Uuid> = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
Row,
|
||||
@@ -1137,9 +1133,9 @@ impl Trip {
|
||||
for unsynced_item in &unsynced_items {
|
||||
let item_id = unsynced_item.to_string();
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"
|
||||
@@ -1172,11 +1168,7 @@ impl Trip {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn load_categories(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn load_categories(&mut self, ctx: &Context, pool: &db::Pool) -> Result<(), Error> {
|
||||
let mut categories: Vec<TripCategory> = vec![];
|
||||
// we can ignore the return type as we collect into `categories`
|
||||
// in the `map_ok()` closure
|
||||
@@ -1241,9 +1233,9 @@ impl Trip {
|
||||
}
|
||||
|
||||
let rows = crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
Row,
|
||||
@@ -1408,12 +1400,12 @@ impl TryFrom<TripTypeRow> for TripType {
|
||||
|
||||
impl TripsType {
|
||||
#[tracing::instrument]
|
||||
pub async fn all(ctx: &Context, pool: &sqlite::Pool) -> Result<Vec<Self>, Error> {
|
||||
pub async fn all(ctx: &Context, pool: &db::Pool) -> Result<Vec<Self>, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
crate::query_all!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
DbTripsTypesRow,
|
||||
@@ -1429,14 +1421,14 @@ impl TripsType {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn save(ctx: &Context, pool: &sqlite::Pool, name: &str) -> Result<Uuid, Error> {
|
||||
pub async fn save(ctx: &Context, pool: &db::Pool, name: &str) -> Result<Uuid, Error> {
|
||||
let user_id = ctx.user.id.to_string();
|
||||
let id = Uuid::new_v4();
|
||||
let id_param = id.to_string();
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"INSERT INTO trips_types
|
||||
@@ -1455,7 +1447,7 @@ impl TripsType {
|
||||
#[tracing::instrument]
|
||||
pub async fn set_name(
|
||||
ctx: &Context,
|
||||
pool: &sqlite::Pool,
|
||||
pool: &db::Pool,
|
||||
id: Uuid,
|
||||
new_name: &str,
|
||||
) -> Result<bool, Error> {
|
||||
@@ -1463,9 +1455,9 @@ impl TripsType {
|
||||
let id_param = id.to_string();
|
||||
|
||||
let result = crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Update,
|
||||
component: sqlite::Component::Trips,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Update,
|
||||
component: db::Component::Trips,
|
||||
},
|
||||
pool,
|
||||
"UPDATE trips_types
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::sqlite;
|
||||
use crate::db;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
@@ -42,9 +42,9 @@ impl User {
|
||||
name: &str,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
crate::query_one!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Select,
|
||||
component: sqlite::Component::User,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Select,
|
||||
component: db::Component::User,
|
||||
},
|
||||
pool,
|
||||
DbUserRow,
|
||||
@@ -57,14 +57,14 @@ impl User {
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn create(pool: &sqlite::Pool, user: NewUser<'_>) -> Result<Uuid, Error> {
|
||||
pub async fn create(pool: &db::Pool, user: NewUser<'_>) -> Result<Uuid, Error> {
|
||||
let id = Uuid::new_v4();
|
||||
let id_param = id.to_string();
|
||||
|
||||
crate::execute!(
|
||||
&sqlite::QueryClassification {
|
||||
query_type: sqlite::QueryType::Insert,
|
||||
component: sqlite::Component::User,
|
||||
&db::QueryClassification {
|
||||
query_type: db::QueryType::Insert,
|
||||
component: db::Component::User,
|
||||
},
|
||||
pool,
|
||||
"INSERT INTO users
|
||||
|
||||
@@ -2,6 +2,14 @@ use maud::{html, Markup};
|
||||
|
||||
use crate::models;
|
||||
use crate::ClientState;
|
||||
use crate::{
|
||||
elements::{
|
||||
self,
|
||||
list::{self, Action, List},
|
||||
},
|
||||
models::inventory::Item,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Inventory;
|
||||
@@ -58,96 +66,64 @@ impl InventoryCategoryList {
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
html!(
|
||||
table
|
||||
#category-list
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
struct Row<'a> {
|
||||
category: &'a models::inventory::Category,
|
||||
active: bool,
|
||||
biggest_category_weight: i64,
|
||||
}
|
||||
impl<'a> list::Row for Row<'a> {
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
colgroup {
|
||||
col style="width:50%" {}
|
||||
col style="width:50%" {}
|
||||
}
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" ."w-3/5" { "Name" }
|
||||
th ."border" ."p-2" { "Weight" }
|
||||
fn cells(&self) -> Vec<list::Cell> {
|
||||
vec![
|
||||
list::Cell {
|
||||
cell_type: list::CellType::Link(list::Link {
|
||||
text: &self.category.name,
|
||||
href: format!("/inventory/category/{}", self.category.id),
|
||||
hx_config: Some(elements::HxConfig {
|
||||
hx_post: format!(
|
||||
"/inventory/categories/{}/select",
|
||||
self.category.id
|
||||
),
|
||||
hx_swap: elements::HxSwap::OuterHtml,
|
||||
hx_target: "#pkglist-item-manager",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
list::Cell {
|
||||
cell_type: list::CellType::NumberWithBar(list::NumberWithBar {
|
||||
value: self.category.total_weight(),
|
||||
max_value: self.biggest_category_weight,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for category in categories {
|
||||
@let active = active_category.map_or(false, |c| category.id == c.id);
|
||||
tr
|
||||
."h-10"
|
||||
."hover:bg-gray-100"
|
||||
."m-3"
|
||||
."h-full"
|
||||
."outline"[active]
|
||||
."outline-2"[active]
|
||||
."outline-indigo-300"[active]
|
||||
."pointer-events-none"[active]
|
||||
{
|
||||
|
||||
td
|
||||
class=@if active_category.map_or(false, |c| category.id == c.id) {
|
||||
"border p-0 m-0 font-bold"
|
||||
} @else {
|
||||
"border p-0 m-0"
|
||||
} {
|
||||
a
|
||||
id="select-category"
|
||||
href={
|
||||
"/inventory/category/"
|
||||
(category.id) "/"
|
||||
List {
|
||||
id: Some("category-list"),
|
||||
editing_config: None,
|
||||
header: list::Header {
|
||||
cells: vec![
|
||||
Some(list::HeaderCell { title: "Name" }),
|
||||
Some(list::HeaderCell { title: "Weight" }),
|
||||
],
|
||||
},
|
||||
rows: categories
|
||||
.iter()
|
||||
.map(|category| {
|
||||
let active = active_category.map_or(false, |c| category.id == c.id);
|
||||
Row {
|
||||
category,
|
||||
active,
|
||||
biggest_category_weight,
|
||||
}
|
||||
hx-post={
|
||||
"/inventory/categories/"
|
||||
(category.id)
|
||||
"/select"
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#pkglist-item-manager"
|
||||
."inline-block" ."p-2" ."m-0" ."w-full"
|
||||
{
|
||||
(category.name.clone())
|
||||
}
|
||||
}
|
||||
td ."border" ."p-2" ."m-0" style="position:relative;" {
|
||||
p {
|
||||
(category.total_weight().to_string())
|
||||
}
|
||||
div ."bg-blue-600" ."h-1.5"
|
||||
style=(
|
||||
format!(
|
||||
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
|
||||
width=(
|
||||
(category.total_weight() as f64)
|
||||
/ (biggest_category_weight as f64)
|
||||
* 100.0
|
||||
)
|
||||
)
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
tr ."h-10" ."bg-gray-300" ."font-bold" {
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
p ."p-2" ."m-2" { "Sum" }
|
||||
}
|
||||
td ."border" ."p-0" ."m-0" {
|
||||
p ."p-2" ."m-2" {
|
||||
(categories.iter().map(models::inventory::Category::total_weight).sum::<i64>().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +138,74 @@ impl InventoryItemList {
|
||||
)]
|
||||
pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup {
|
||||
let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1);
|
||||
|
||||
struct Row<'a> {
|
||||
item: &'a Item,
|
||||
biggest_item_weight: i64,
|
||||
edit_item_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl<'a> list::Row for Row<'a> {
|
||||
fn cells(&self) -> Vec<list::Cell> {
|
||||
vec![
|
||||
list::Cell {
|
||||
cell_type: list::CellType::Link(list::Link {
|
||||
text: &self.item.name,
|
||||
href: format!("/inventory/item/{id}/", id = self.item.id),
|
||||
hx_config: None,
|
||||
}),
|
||||
},
|
||||
list::Cell {
|
||||
cell_type: list::CellType::NumberWithBar(list::NumberWithBar {
|
||||
value: self.item.weight,
|
||||
max_value: self.biggest_item_weight,
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn is_edit(&self) -> bool {
|
||||
self.edit_item_id.map_or(false, |id| id == self.item.id)
|
||||
}
|
||||
}
|
||||
|
||||
let table = list::List {
|
||||
id: None,
|
||||
editing_config: Some(Box::new(|row: Row| list::EditingConfig {
|
||||
edit_action: Action::Href(format!("?edit_item={id}", id = row.item.id)),
|
||||
edit_hx_config: None,
|
||||
|
||||
delete_action: Action::Href(format!(
|
||||
"/inventory/item/{id}/delete",
|
||||
id = row.item.id
|
||||
)),
|
||||
delete_hx_config: None,
|
||||
|
||||
save_action: Action::Submit("edit-item"),
|
||||
save_hx_config: None,
|
||||
|
||||
cancel_action: Action::Href(format!(
|
||||
"/inventory/item/{id}/cancel",
|
||||
id = row.item.id
|
||||
)),
|
||||
cancel_hx_config: None,
|
||||
})),
|
||||
header: list::Header {
|
||||
cells: vec![
|
||||
Some(list::HeaderCell { title: "Name" }),
|
||||
Some(list::HeaderCell { title: "Weight" }),
|
||||
],
|
||||
},
|
||||
rows: items
|
||||
.iter()
|
||||
.map(|item| Row {
|
||||
item,
|
||||
biggest_item_weight,
|
||||
edit_item_id,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
html!(
|
||||
div #items {
|
||||
@if items.is_empty() {
|
||||
@@ -176,6 +220,7 @@ impl InventoryItemList {
|
||||
method="post"
|
||||
{}
|
||||
}
|
||||
(table.render())
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
@@ -352,7 +397,6 @@ impl InventoryNewItemFormName {
|
||||
hx-target="this"
|
||||
hx-params="new-item-name"
|
||||
hx-swap="outerHTML"
|
||||
#abc
|
||||
{
|
||||
label for="name" .font-bold { "Name" }
|
||||
input
|
||||
|
||||
Reference in New Issue
Block a user