Move away from models and manage subpackage

This architecture was started when the project still used Diesel.
Now that it uses SQLx, less things are done in Rust and more are done in
SQL. This commit now moves more of the query logic into SQL, which
should lead to more efficient queries and less moving data around.
This commit is contained in:
Simon Bruder 2024-07-21 23:43:53 +02:00
parent b7adf03dcc
commit 5b3dd34312
Signed by: simon
GPG key ID: 347FF8699CDA0776
58 changed files with 1072 additions and 1512 deletions

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.original_packaging = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class_name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "02438c29c249ce2b446d15a41d07c40ce73946e62744a486cbbac256c4a4cf55"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE parent = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "2e9619ce6db1047e2f5447c4925f28fd05f18706f70220b6ebb7354d2a0a9e3b"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id AS \"id?\", to_char(short_id, '000000') AS \"short_id?\"\n FROM items\n WHERE id = ANY ($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id?",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "short_id?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false,
null
]
},
"hash": "460f15b3c5ec2774c237fc581166bf44ecdb0b6145f8abba155478643a474125"
}

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.class = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class_name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "482df01f0509cc8ec18ffe1caea8f65f11c170b67424e35697540ae12e577227"
}

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, name FROM item_classes WHERE parent = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false
]
},
"hash": "4ed0c3101202abc920d81ee6acdaff9ed698f585df5e798d1f42206042e31a1b"
}

View file

@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO items (name, parent, class, original_packaging, description)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Uuid",
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "53be3ef02ba8b3deb3a74b184a4eb93e01fd0983c19aff3fca67bdc0afab4f37"
}

View file

@ -1,62 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items ORDER BY created_at",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "58969747bdccae4a2d3ad8d8117aa92283151d67f52fbb22e5d976b1c6a5c367"
}

View file

@ -0,0 +1,56 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || items.name,\n cte.parent_class_names || item_classes.name\n FROM items\n JOIN cte\n ON items.parent = cte.id\n JOIN item_classes\n ON items.class = item_classes.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n ORDER BY items.created_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class_name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "parents!",
"type_info": "UuidArray"
},
{
"ordinal": 5,
"name": "parent_names!: Vec<Option<String>>",
"type_info": "VarcharArray"
},
{
"ordinal": 6,
"name": "parent_class_names!",
"type_info": "VarcharArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
null,
true,
false,
false,
null,
null,
null
]
},
"hash": "5dac9aa215abdc07cda1f4d46db4bcafec45b6264efcb8a175580a71e48c7421"
}

View file

@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE items\n SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Uuid",
"Uuid",
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "6737f54ceb18b9b744bac838801edbbdfe2cbf68a6346f93c072d47f5add9e46"
}

View file

@ -1,48 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO item_classes (name, parent, description) VALUES ($1, $2, $3) RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true,
false,
false
]
},
"hash": "68d93c71840f87d5f5bb14b4d7fc34edd6be47cd1706b3612a332bbfd4bb54b4"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT type as \"type!\" FROM\n (SELECT id, 'item' AS \"type\" FROM items\n UNION ALL\n SELECT id, 'item_class' AS \"type\" FROM item_classes) id_mapping\n WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "type!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "6dd8b63f4aaefbc1ab5d5a9bae2338a7275ba56a9b17278fd886175c3a27b0dd"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT type as \"type!\"\n FROM (SELECT id, 'item' AS \"type\" FROM items\n UNION ALL\n SELECT id, 'item_class' AS \"type\" FROM item_classes) id_mapping\n WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "type!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "719aff68ead7ada499158fec5e9f8f3c3841a4424da04aee0136c7e4f8df79e7"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE id = ANY ($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "79d8bfe2ed76ee550cdc31f282f598749d931af69a80d24f4575a4bc2c740f3b"
}

View file

@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class_name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
false
]
},
"hash": "7b3f478c9b217e09043daeca5dc574381493e559860258f4f6bffb12825b1ed7"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE short_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "7eeb752c8b00ac4000104f0254186f1f9fdb076e8f8b98f10fc1b981cfe8038c"
}

View file

@ -1,69 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE items SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6 WHERE id = $1 RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Uuid",
"Uuid",
"Uuid",
"Varchar"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "82df5c0633a655d376b8a91e9f11981cfee40fd04cb4e3552cc5f4ebf4ed0572"
}

View file

@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE item_classes\n SET name = $2, parent = $3, description = $4\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Uuid",
"Varchar"
]
},
"nullable": []
},
"hash": "835261895368c22af5780c4e4b69b518f5cb6936e3e4ea23c200b13787a401e1"
}

View file

@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO item_classes (name, parent, description)\n VALUES ($1, $2, $3)\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Varchar"
]
},
"nullable": [
false
]
},
"hash": "83fe5e57b2c7db1fdfbf05749646ac6ebe71d81057cd865c5f946d6ddb62b552"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT * FROM item_classes WHERE parent = $1", "query": "SELECT\n class.id,\n class.name,\n class.description,\n class.parent,\n parent.name AS \"parent_name?\"\n FROM item_classes AS \"class\"\n LEFT JOIN item_classes AS \"parent\"\n ON class.parent = parent.id\n WHERE class.id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -15,17 +15,17 @@
}, },
{ {
"ordinal": 2, "ordinal": 2,
"name": "description",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "parent", "name": "parent",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{ {
"ordinal": 4, "ordinal": 4,
"name": "description", "name": "parent_name?",
"type_info": "Varchar" "type_info": "Varchar"
} }
], ],
@ -37,10 +37,10 @@
"nullable": [ "nullable": [
false, false,
false, false,
true,
false, false,
true,
false false
] ]
}, },
"hash": "c552c0a40bc8995cb95726a85f1d0c0b86eb2322035e6a720e2e6d425072a8c1" "hash": "84b4620db57dd9b963e09153c3de5938b3959ae41744098c4e9565404abf09ae"
} }

View file

@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO items (name, parent, class, original_packaging, description)\n SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])\n RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"VarcharArray",
"UuidArray",
"UuidArray",
"UuidArray",
"VarcharArray"
]
},
"nullable": [
false
]
},
"hash": "857bbaea0fa73c37679d927aa627ee841c5b89c86af12f32493ba05c0f0ead91"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.*\n FROM items\n INNER JOIN\n unnest((SELECT parents FROM item_tree WHERE id = $1))\n WITH ORDINALITY AS parents(id, n)\n ON items.id = parents.id\n ORDER BY parents.n;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "9286b6ee0c08b8446ede68b890b8bf3208b55b51433ec92b4e7a452929a81945"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE class = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "94958a3a57c3178e6b0de5723b1fbc5433e972b5522b367098afe6cb90a30bf2"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT unnest(parents) as \"parents!\" FROM item_tree WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "parents!",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "97d6a7ee24e75dc5a9dc41a581e1013767fe36575c28574733c5ab5cbf557fb5"
}

View file

@ -0,0 +1,70 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n items.id,\n items.short_id,\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n items.original_packaging,\n op.name AS \"original_packaging_name?\",\n op_class.name AS \"original_packaging_class_name?\",\n items.description\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n LEFT JOIN items AS \"op\"\n ON items.original_packaging = op.id\n LEFT JOIN item_classes AS \"op_class\"\n ON op.class = op_class.id\n WHERE items.id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "class_name",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 6,
"name": "original_packaging_name?",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "original_packaging_class_name?",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
true,
false,
false,
true,
true,
false,
false
]
},
"hash": "9c1a1e9c33539f6ec4800c9e8fc8ce3b65f49c34ad848321dc1b151c21cb0043"
}

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.parent = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class_name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "9e1704d0b60906061460e2c972fccf5d87b053857c161651360e5a8fdd80e397"
}

View file

@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id as \"id!\", parents as \"parents!\" FROM item_tree",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "parents!",
"type_info": "UuidArray"
}
],
"parameters": {
"Left": []
},
"nullable": [
true,
true
]
},
"hash": "a6f646089b4424deffc0b1a454bcfa2f2a497180e2517a937471f81a1f9c5538"
}

View file

@ -1,68 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO items (name, parent, class, original_packaging, description)\n SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])\n RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"VarcharArray",
"UuidArray",
"UuidArray",
"UuidArray",
"VarcharArray"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "b54d323092a1dc2e34d3882a06b3cd119362d0baf5effbbd09f969ee31f385ef"
}

View file

@ -1,68 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO items (name, parent, class, original_packaging, description) VALUES ($1, $2, $3, $4, $5) RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Uuid",
"Uuid",
"Varchar"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "b80bb07ab582b0705b5c0370066730edf887d66a4196a0834c59f0df9f9314d3"
}

View file

@ -1,45 +1,35 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE id = $1", "query": "SELECT\n items.name,\n items.parent,\n items.class,\n item_classes.name AS \"class_name\",\n items.original_packaging,\n items.description\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
"ordinal": 0, "ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name", "name": "name",
"type_info": "Varchar" "type_info": "Varchar"
}, },
{ {
"ordinal": 2, "ordinal": 1,
"name": "parent", "name": "parent",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{ {
"ordinal": 3, "ordinal": 2,
"name": "class", "name": "class",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{
"ordinal": 3,
"name": "class_name",
"type_info": "Varchar"
},
{ {
"ordinal": 4, "ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging", "name": "original_packaging",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{ {
"ordinal": 7, "ordinal": 5,
"name": "description", "name": "description",
"type_info": "Varchar" "type_info": "Varchar"
} }
@ -50,15 +40,13 @@
] ]
}, },
"nullable": [ "nullable": [
false,
true, true,
true, true,
false, false,
false, false,
false,
true, true,
false false
] ]
}, },
"hash": "1e9045f52c002a19b815351fee1c7ee7520478ff4f5886b4523fb0dc4df0e204" "hash": "c49b88eda9a62743783bc894f01bb6198594f94a3e0856abde0efdb4e49dbab8"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT * FROM item_classes ORDER BY created_at", "query": "SELECT class.id, class.name, class.parent, parent.name AS \"parent_name?\"\n FROM item_classes AS \"class\"\n LEFT JOIN item_classes AS \"parent\"\n ON class.parent = parent.id\n ORDER BY class.created_at\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -20,12 +20,7 @@
}, },
{ {
"ordinal": 3, "ordinal": 3,
"name": "created_at", "name": "parent_name?",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Varchar" "type_info": "Varchar"
} }
], ],
@ -36,9 +31,8 @@
false, false,
false, false,
true, true,
false,
false false
] ]
}, },
"hash": "6e7b3389c47091d9fc8c7638b401b413f804c6f3e082a818b67ebab0938acb39" "hash": "c7e35dee5f56da8da3c083e191f396b95917b15768ebe0250ce7840391036616"
} }

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT id FROM item_classes ORDER BY created_at", "query": "SELECT id FROM items WHERE short_id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -10,11 +10,13 @@
} }
], ],
"parameters": { "parameters": {
"Left": [] "Left": [
"Int4"
]
}, },
"nullable": [ "nullable": [
false false
] ]
}, },
"hash": "5cf503740d71431d8b9d256960c0ce194ede48a1c46326315cae9f07347597f6" "hash": "cfce4ef19a3e0a3a0582d3c7d266b952397a017b33c351c5a33f1daa78ab908a"
} }

View file

@ -1,49 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE item_classes SET name = $2, parent = $3, description = $4 WHERE id = $1 RETURNING *",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Uuid",
"Varchar"
]
},
"nullable": [
false,
false,
true,
false,
false
]
},
"hash": "e0129afa95f896d79f772fb177a8f9229dfbbfd289039db7b733c7d1d050f4bf"
}

View file

@ -1,64 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM items WHERE original_packaging = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "parent",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "class",
"type_info": "Uuid"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "short_id",
"type_info": "Int4"
},
{
"ordinal": 6,
"name": "original_packaging",
"type_info": "Uuid"
},
{
"ordinal": 7,
"name": "description",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
true,
false,
false,
false,
true,
false
]
},
"hash": "efe2258db42b60f732d562d106842b19308d9a558703b02e758f60e7d8644d00"
}

View file

@ -1,30 +1,20 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT * FROM item_classes WHERE id = $1", "query": "SELECT name, parent, description FROM item_classes WHERE id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
"ordinal": 0, "ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name", "name": "name",
"type_info": "Varchar" "type_info": "Varchar"
}, },
{ {
"ordinal": 2, "ordinal": 1,
"name": "parent", "name": "parent",
"type_info": "Uuid" "type_info": "Uuid"
}, },
{ {
"ordinal": 3, "ordinal": 2,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "description", "name": "description",
"type_info": "Varchar" "type_info": "Varchar"
} }
@ -35,12 +25,10 @@
] ]
}, },
"nullable": [ "nullable": [
false,
false, false,
true, true,
false,
false false
] ]
}, },
"hash": "308962c26250f9312287a3f2f21e5da76e4cf488eedd4704019b4f14b6fbafb2" "hash": "f446c6ab881c166102f3b20056e0709be6fbcfc596063878c0bad9df00037dbe"
} }

View file

@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, name FROM item_classes",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "fc5e536bddb5d76021164ae4f5a6b0ad52d1ae047fd004f1359be7122facf8e7"
}

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN unnest((SELECT parents FROM item_tree WHERE id = $1))\n WITH ORDINALITY AS parents(id, n)\n ON items.id = parents.id\n JOIN item_classes\n ON items.class = item_classes.id\n ORDER BY parents.n",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "class_name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
true,
false
]
},
"hash": "ff39480d9395b357d71e9af3eb37fa308f4df6a0ca6442fa7f9bbda1e34ffbbe"
}

10
Cargo.lock generated
View file

@ -1113,6 +1113,15 @@ version = "1.70.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -1165,6 +1174,7 @@ dependencies = [
"enum-iterator", "enum-iterator",
"env_logger", "env_logger",
"futures-util", "futures-util",
"itertools",
"log", "log",
"maud", "maud",
"mime", "mime",

View file

@ -19,6 +19,7 @@ datamatrix = "0.3.1"
enum-iterator = "2.1.0" enum-iterator = "2.1.0"
env_logger = "0.11.3" env_logger = "0.11.3"
futures-util = "0.3.30" futures-util = "0.3.30"
itertools = "0.13.0"
log = "0.4.21" log = "0.4.21"
maud = { version = "0.26.0", features = ["actix-web"] } maud = { version = "0.26.0", features = ["actix-web"] }
mime = "0.3.17" mime = "0.3.17"

View file

@ -8,12 +8,10 @@ use actix_identity::Identity;
use actix_web::{error, get, post, web, HttpRequest, Responder}; use actix_web::{error, get, post, web, HttpRequest, Responder};
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::{query_scalar, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{self, datalist, forms, helpers::PageActionGroup, TemplateConfig}; use crate::frontend::templates::{self, datalist, forms, helpers::PageActionGroup, TemplateConfig};
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get).service(post); cfg.service(get).service(post);
@ -130,51 +128,64 @@ async fn post(
user: Identity, user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let data = data.into_inner(); let data = data.into_inner();
let new_item = NewItem {
name: data.name,
class: data.class,
parent: data.parent,
original_packaging: data.original_packaging,
description: data.description,
};
if data.quantity == 1 { if data.quantity == 1 {
let item = manage::item::add(&pool, new_item) query_scalar!(
.await "INSERT INTO items (name, parent, class, original_packaging, description)
.map_err(error::ErrorInternalServerError)?; VALUES ($1, $2, $3, $4, $5)
Ok( RETURNING id",
web::Redirect::to("/item/".to_owned() + &item.id.to_string()) data.name,
data.parent,
data.class,
data.original_packaging,
data.description
)
.fetch_one(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)
.map(|id| {
web::Redirect::to("/item/".to_owned() + &id.to_string())
.see_other() .see_other()
.respond_to(&req) .respond_to(&req)
.map_into_boxed_body(), .map_into_boxed_body()
) })
} else { } else {
let items = manage::item::add_multiple(&pool, new_item, data.quantity) query_scalar!(
.await "INSERT INTO items (name, parent, class, original_packaging, description)
.map_err(error::ErrorInternalServerError)?; SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])
Ok(templates::base( RETURNING id",
TemplateConfig { &vec![data.name; data.quantity] as &[Option<String>],
path: "/items/add", &vec![data.parent; data.quantity] as &[Option<Uuid>],
title: Some("Added Items"), &vec![data.class; data.quantity],
page_title: Some(Box::new("Added Items")), &vec![data.original_packaging; data.quantity] as &[Option<Uuid>],
page_actions: vec![PageActionGroup::generate_labels( &vec![data.description; data.quantity]
&items.iter().map(|i| i.id).collect::<Vec<Uuid>>(), )
)], .fetch_all(pool.as_ref())
user: Some(user), .await
..Default::default() .map_err(error::ErrorInternalServerError)
}, .map(|ids| {
html! { templates::base(
ul { TemplateConfig {
@for item in items { path: "/items/add",
li { title: Some("Added Items"),
a href={ "/item/" (item.id) } { (item.id) } page_title: Some(Box::new("Added Items")),
page_actions: vec![PageActionGroup::generate_labels(&ids)],
user: Some(user),
..Default::default()
},
html! {
ul {
@for id in &ids {
li {
a href={ "/item/" (id) } { (id) }
}
} }
} }
}
a href="/items" { "Back to all items" } a href="/items" { "Back to all items" }
}, },
) )
.respond_to(&req) .respond_to(&req)
.map_into_boxed_body()) .map_into_boxed_body()
})
} }
} }

View file

@ -4,11 +4,9 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, post, web, Responder}; use actix_web::{error, post, web, Responder};
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(post); cfg.service(post);
} }
@ -21,7 +19,8 @@ async fn post(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
manage::item::delete(&pool, id) query!("DELETE FROM items WHERE id = $1", id)
.execute(pool.as_ref())
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;

View file

@ -7,17 +7,25 @@ use std::fmt::Display;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder}; use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use serde::Deserialize;
use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{self, datalist, forms, helpers::ItemName, TemplateConfig}; use crate::frontend::templates::{self, datalist, forms, helpers::ItemName, TemplateConfig};
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get).service(post); cfg.service(get).service(post);
} }
#[derive(Deserialize)]
struct ItemEditForm {
name: Option<String>,
parent: Option<Uuid>,
class: Uuid,
original_packaging: Option<Uuid>,
description: String,
}
#[get("/item/{id}/edit")] #[get("/item/{id}/edit")]
async fn get( async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@ -26,13 +34,35 @@ async fn get(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item = manage::item::get(&pool, id) let (item_name, form) = query!(
.await r#"SELECT
.map_err(error::ErrorInternalServerError)?; items.name,
items.parent,
let item_class = manage::item_class::get(&pool, item.class) items.class,
.await item_classes.name AS "class_name",
.map_err(error::ErrorInternalServerError)?; items.original_packaging,
items.description
FROM items
JOIN item_classes
ON items.class = item_classes.id
WHERE items.id = $1"#,
id
)
.map(|row| {
(
ItemName::new(row.name.as_ref(), &row.class_name),
ItemEditForm {
name: row.name,
parent: row.parent,
class: row.class,
original_packaging: row.original_packaging,
description: row.description,
},
)
})
.fetch_one(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let datalist_items = datalist::items(&pool) let datalist_items = datalist::items(&pool)
.await .await
@ -42,13 +72,12 @@ async fn get(
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
let mut title = item_name.to_string(); let mut title = item_name.to_string();
title.push_str(" Edit Item"); title.push_str(" Edit Item");
Ok(templates::base( Ok(templates::base(
TemplateConfig { TemplateConfig {
path: &format!("/item/{}/edit", item.id), path: &format!("/item/{}/edit", id),
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_name.clone())), page_title: Some(Box::new(item_name.clone())),
datalists: vec![&datalist_items, &datalist_item_classes], datalists: vec![&datalist_items, &datalist_item_classes],
@ -63,7 +92,7 @@ async fn get(
title: "UUID", title: "UUID",
required: true, required: true,
disabled: true, disabled: true,
value: Some(&item.id), value: Some(&id),
..Default::default() ..Default::default()
}) })
(forms::InputGroup { (forms::InputGroup {
@ -71,8 +100,8 @@ async fn get(
name: "name", name: "name",
title: "Name", title: "Name",
optional: true, optional: true,
disabled: item.name.is_none(), disabled: form.name.is_none(),
value: item.name.as_ref().map(|s| s as &dyn Display), value: form.name.as_ref().map(|s| s as &dyn Display),
..Default::default() ..Default::default()
}) })
(forms::InputGroup { (forms::InputGroup {
@ -80,7 +109,7 @@ async fn get(
name: "class", name: "class",
title: "Class", title: "Class",
required: true, required: true,
value: Some(&item.class), value: Some(&form.class),
datalist: Some(&datalist_item_classes), datalist: Some(&datalist_item_classes),
..Default::default() ..Default::default()
}) })
@ -89,8 +118,8 @@ async fn get(
name: "parent", name: "parent",
title: "Parent", title: "Parent",
optional: true, optional: true,
value: item.parent.as_ref().map(|id| id as &dyn Display), value: form.parent.as_ref().map(|id| id as &dyn Display),
disabled: item.parent.is_none(), disabled: form.parent.is_none(),
datalist: Some(&datalist_items), datalist: Some(&datalist_items),
..Default::default() ..Default::default()
}) })
@ -99,8 +128,8 @@ async fn get(
name: "original_packaging", name: "original_packaging",
title: "Original Packaging", title: "Original Packaging",
optional: true, optional: true,
value: item.original_packaging.as_ref().map(|id| id as &dyn Display), value: form.original_packaging.as_ref().map(|id| id as &dyn Display),
disabled: item.original_packaging.is_none(), disabled: form.original_packaging.is_none(),
datalist: Some(&datalist_items), datalist: Some(&datalist_items),
..Default::default() ..Default::default()
}) })
@ -108,7 +137,7 @@ async fn get(
r#type: forms::InputType::Textarea, r#type: forms::InputType::Textarea,
name: "description", name: "description",
title: "Description ", title: "Description ",
value: Some(&item.description), value: Some(&form.description),
..Default::default() ..Default::default()
}) })
@ -122,14 +151,25 @@ async fn get(
async fn post( async fn post(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
data: web::Form<NewItem>, data: web::Form<ItemEditForm>,
_user: Identity, _user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item = manage::item::update(&pool, id, data.into_inner()) query!(
.await "UPDATE items
.map_err(error::ErrorInternalServerError)?; SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6
WHERE id = $1",
id,
data.name,
data.parent,
data.class,
data.original_packaging,
data.description
)
.execute(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item/".to_owned() + &item.id.to_string()).see_other()) Ok(web::Redirect::to("/item/".to_owned() + &id.to_string()).see_other())
} }

View file

@ -2,12 +2,10 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{ use crate::frontend::templates::{
@ -15,44 +13,73 @@ use crate::frontend::templates::{
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod}, helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
TemplateConfig, TemplateConfig,
}; };
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get); cfg.service(get);
} }
struct ItemListEntry {
id: Uuid,
name: ItemName,
class: Uuid,
class_name: String,
parents: Vec<ItemPreview>,
}
#[get("/items")] #[get("/items")]
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> { async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
let item_list = manage::item::get_all(&pool) let items = query!(
.await r#"
.map_err(error::ErrorInternalServerError)?; WITH RECURSIVE cte AS (
SELECT
id,
ARRAY[]::UUID[] AS parents,
ARRAY[]::VARCHAR[] AS parent_names,
ARRAY[]::VARCHAR[] AS parent_class_names
FROM items
WHERE parent IS NULL
let items = manage::item::get_all_as_map(&pool) UNION
.await
.map_err(error::ErrorInternalServerError)?;
let item_classes = manage::item_class::get_all_as_map(&pool) SELECT
.await items.id,
.map_err(error::ErrorInternalServerError)?; cte.parents || items.parent,
cte.parent_names || items.name,
let item_tree = manage::item::get_all_parents(&pool) cte.parent_class_names || item_classes.name
.await FROM items
.map_err(error::ErrorInternalServerError)?; JOIN cte
ON items.parent = cte.id
// TODO: remove clone (should be possible without it) JOIN item_classes
let item_parents: HashMap<Uuid, Vec<Item>> = item_tree ON items.class = item_classes.id
.iter() )
.map(|(id, parent_ids)| { SELECT
( cte.id AS "id!",
*id, items.name,
parent_ids items.class,
.iter() item_classes.name AS "class_name",
.map(|parent_id| items.get(parent_id).unwrap().clone()) cte.parents AS "parents!",
.collect(), cte.parent_names AS "parent_names!: Vec<Option<String>>",
) cte.parent_class_names AS "parent_class_names!"
}) FROM cte
.collect(); JOIN items
ON cte.id = items.id
JOIN item_classes
ON items.class = item_classes.id
ORDER BY items.created_at
"#
)
.map(|row| ItemListEntry {
id: row.id,
name: ItemName::new(row.name.as_ref(), &row.class_name),
class: row.class,
class_name: row.class_name,
parents: itertools::izip!(row.parents, row.parent_names, row.parent_class_names)
.map(|(id, name, class_name)| ItemPreview::from_parts(id, name.as_ref(), &class_name))
.collect(),
})
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
Ok(templates::base( Ok(templates::base(
TemplateConfig { TemplateConfig {
@ -82,24 +109,11 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
} }
} }
tbody { tbody {
@for item in item_list { @for item in items {
@let class = item_classes.get(&item.class).unwrap();
@let parents = item_parents.get(&item.id).unwrap();
tr { tr {
td { td { (ItemPreview::new(item.id, item.name.clone().terse())) }
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &class.name).terse())) td { a href={ "/item-class/" (item.class) } { (item.class_name) } }
} td { (templates::helpers::parents_breadcrumb(item.name, &item.parents, false)) }
td { a href={ "/item-class/" (class.id) } { (class.name) } }
td {
(templates::helpers::parents_breadcrumb(
ItemName::new(
item.name.as_ref(),
&class.name
),
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
false
))
}
} }
} }
} }

View file

@ -5,7 +5,7 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{ use crate::frontend::templates::{
@ -13,12 +13,21 @@ use crate::frontend::templates::{
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod}, helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
TemplateConfig, TemplateConfig,
}; };
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get); cfg.service(get);
} }
struct ItemDetails {
id: Uuid,
short_id: i32,
name: ItemName,
class: Uuid,
class_name: String,
original_packaging: Option<ItemPreview>,
description: String,
}
#[get("/item/{id}")] #[get("/item/{id}")]
async fn get( async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@ -27,46 +36,96 @@ async fn get(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item = manage::item::get(&pool, id) let item = query!(
.await r#"SELECT
.map_err(error::ErrorInternalServerError)?; items.id,
items.short_id,
items.name,
items.class,
item_classes.name AS "class_name",
items.original_packaging,
op.name AS "original_packaging_name?",
op_class.name AS "original_packaging_class_name?",
items.description
FROM items
JOIN item_classes
ON items.class = item_classes.id
LEFT JOIN items AS "op"
ON items.original_packaging = op.id
LEFT JOIN item_classes AS "op_class"
ON op.class = op_class.id
WHERE items.id = $1"#,
id
)
.map(|row| ItemDetails {
id: row.id,
short_id: row.short_id,
name: ItemName::new(row.name.as_ref(), &row.class_name),
class: row.class,
class_name: row.class_name,
original_packaging: row.original_packaging.map(|id| {
ItemPreview::from_parts(
id,
row.original_packaging_name.as_ref(),
&row.original_packaging_class_name.unwrap(),
)
}),
description: row.description,
})
.fetch_one(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let item_classes = manage::item_class::get_all_as_map(&pool) let parents = query!(
.await r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map_err(error::ErrorInternalServerError)?; FROM items
JOIN unnest((SELECT parents FROM item_tree WHERE id = $1))
WITH ORDINALITY AS parents(id, n)
ON items.id = parents.id
JOIN item_classes
ON items.class = item_classes.id
ORDER BY parents.n"#,
id
)
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let parents = manage::item::get_parents_details(&pool, item.id) let children = query!(
.await r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map_err(error::ErrorInternalServerError)?; FROM items
JOIN item_classes
ON items.class = item_classes.id
WHERE items.parent = $1"#,
id
)
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let children = manage::item::get_children(&pool, item.id) let original_packaging_of = query!(
.await r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map_err(error::ErrorInternalServerError)?; FROM items
JOIN item_classes
ON items.class = item_classes.id
WHERE items.original_packaging = $1"#,
id
)
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let original_packaging = match item.original_packaging { let mut title = item.name.to_string();
Some(id) => Some(
manage::item::get(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?,
),
None => None,
};
let original_packaging_of = manage::item::original_packaging_contents(&pool, id)
.await
.map_err(error::ErrorInternalServerError)?;
let item_class = item_classes.get(&item.class).unwrap();
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
let mut title = item_name.to_string();
title.push_str(" Item Details"); title.push_str(" Item Details");
Ok(templates::base( Ok(templates::base(
TemplateConfig { TemplateConfig {
path: &format!("/item/{}", item.id), path: &format!("/item/{}", item.id),
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_name.clone())), page_title: Some(Box::new(item.name.clone())),
page_actions: vec![ page_actions: vec![
(PageActionGroup::Button { (PageActionGroup::Button {
action: PageAction { action: PageAction {
@ -109,32 +168,21 @@ async fn get(
} }
tr { tr {
th { "Name" } th { "Name" }
td { (item_name.clone().terse()) } td { (item.name.clone().terse()) }
} }
tr { tr {
th { "Class" } th { "Class" }
td { a href={ "/item-class/" (item.class) } { (item_class.name) } } td { a href={ "/item-class/" (item.class) } { (item.class_name) } }
} }
tr { tr {
th { "Parents" } th { "Parents" }
td { td { (templates::helpers::parents_breadcrumb(item.name, &parents, true)) }
(templates::helpers::parents_breadcrumb(
ItemName::new(
item.name.as_ref(),
&item_class.name
),
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
true
))
}
} }
tr { tr {
th { "Original Packaging" } th { "Original Packaging" }
td { td {
@if let Some(original_packaging) = original_packaging { @if let Some(original_packaging) = item.original_packaging {
a (original_packaging)
href={ "/item/" (original_packaging.id) }
{ (ItemName::new(original_packaging.name.as_ref(), &item_classes.get(&original_packaging.class).unwrap().name)) }
} @else { } @else {
"-" "-"
} }
@ -153,16 +201,14 @@ async fn get(
} }
div { div {
(PageActionGroup::generate_labels( (PageActionGroup::generate_labels(
&children.iter().map(|i| i.id).collect::<Vec<Uuid>>(), &children.iter().map(|ip| ip.id).collect::<Vec<Uuid>>(),
)) ))
} }
} }
ul { ul {
@for child in children { @for child in children {
li { li { (child) }
(ItemPreview::from_parts(child.id, child.name.as_ref(), &item_classes.get(&child.class).unwrap().name))
}
} }
} }
} }
@ -172,9 +218,7 @@ async fn get(
ul { ul {
@for item in original_packaging_of { @for item in original_packaging_of {
li { li { (item) }
(ItemPreview::from_parts(item.id, item.name.as_ref(), &item_classes.get(&item.class).unwrap().name))
}
} }
} }
} }

View file

@ -8,12 +8,10 @@ use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder}; use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::{query_scalar, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{self, datalist, forms, TemplateConfig}; use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get).service(post); cfg.service(get).service(post);
@ -93,15 +91,18 @@ async fn post(
_user: Identity, _user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let data = data.into_inner(); let data = data.into_inner();
let item = manage::item_class::add(
&pool, let id = query_scalar!(
NewItemClass { "INSERT INTO item_classes (name, parent, description)
name: data.name, VALUES ($1, $2, $3)
parent: data.parent, RETURNING id",
description: data.description, data.name,
}, data.parent,
data.description
) )
.fetch_one(pool.as_ref())
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
Ok(web::Redirect::to("/item-class/".to_owned() + &id.to_string()).see_other())
} }

View file

@ -4,11 +4,9 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, post, web, Responder}; use actix_web::{error, post, web, Responder};
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(post); cfg.service(post);
} }
@ -21,7 +19,8 @@ async fn post(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
manage::item_class::delete(&pool, id) query!("DELETE FROM item_classes WHERE id = $1", id)
.execute(pool.as_ref())
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;

View file

@ -7,17 +7,23 @@ use std::fmt::Display;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder}; use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use serde::Deserialize;
use sqlx::{query, query_as, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{self, datalist, forms, TemplateConfig}; use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
use crate::manage;
use crate::models::*;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get).service(post); cfg.service(get).service(post);
} }
#[derive(Deserialize)]
struct ItemClassEditForm {
name: String,
parent: Option<Uuid>,
description: String,
}
#[get("/item-class/{id}/edit")] #[get("/item-class/{id}/edit")]
async fn get( async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@ -26,22 +32,27 @@ async fn get(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item_class = manage::item_class::get(&pool, id) let form = query_as!(
.await ItemClassEditForm,
.map_err(error::ErrorInternalServerError)?; "SELECT name, parent, description FROM item_classes WHERE id = $1",
id
)
.fetch_one(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let datalist_item_classes = datalist::item_classes(&pool) let datalist_item_classes = datalist::item_classes(&pool)
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let mut title = item_class.name.clone(); let mut title = form.name.clone();
title.push_str(" Item Details"); title.push_str(" Item Details");
Ok(templates::base( Ok(templates::base(
TemplateConfig { TemplateConfig {
path: &format!("/items-class/{}/add", id), path: &format!("/items-class/{}/add", id),
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_class.name.clone())), page_title: Some(Box::new(form.name.clone())),
datalists: vec![&datalist_item_classes], datalists: vec![&datalist_item_classes],
user: Some(user), user: Some(user),
..Default::default() ..Default::default()
@ -54,7 +65,7 @@ async fn get(
title: "UUID", title: "UUID",
disabled: true, disabled: true,
required: true, required: true,
value: Some(&item_class.id), value: Some(&id),
..Default::default() ..Default::default()
}) })
(forms::InputGroup { (forms::InputGroup {
@ -62,7 +73,7 @@ async fn get(
name: "name", name: "name",
title: "Name", title: "Name",
required: true, required: true,
value: Some(&item_class.name), value: Some(&form.name),
..Default::default() ..Default::default()
}) })
(forms::InputGroup { (forms::InputGroup {
@ -70,8 +81,8 @@ async fn get(
name: "parent", name: "parent",
title: "Parent", title: "Parent",
optional: true, optional: true,
disabled: item_class.parent.is_none(), disabled: form.parent.is_none(),
value: item_class.parent.as_ref().map(|id| id as &dyn Display), value: form.parent.as_ref().map(|id| id as &dyn Display),
datalist: Some(&datalist_item_classes), datalist: Some(&datalist_item_classes),
..Default::default() ..Default::default()
}) })
@ -79,7 +90,7 @@ async fn get(
r#type: forms::InputType::Textarea, r#type: forms::InputType::Textarea,
name: "description", name: "description",
title: "Description ", title: "Description ",
value: Some(&item_class.description), value: Some(&form.description),
..Default::default() ..Default::default()
}) })
@ -93,14 +104,23 @@ async fn get(
async fn post( async fn post(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
data: web::Form<NewItemClass>, data: web::Form<ItemClassEditForm>,
_user: Identity, _user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item_class = manage::item_class::update(&pool, id, data.into_inner()) query!(
.await "UPDATE item_classes
.map_err(error::ErrorInternalServerError)?; SET name = $2, parent = $3, description = $4
WHERE id = $1",
id,
data.name,
data.parent,
data.description
)
.execute(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other()) Ok(web::Redirect::to("/item-class/".to_owned() + &id.to_string()).see_other())
} }

View file

@ -5,29 +5,45 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid;
use crate::frontend::templates::{ use crate::frontend::templates::{
self, self,
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod}, helpers::{Colour, ItemClassPreview, PageAction, PageActionGroup, PageActionMethod},
TemplateConfig, TemplateConfig,
}; };
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get); cfg.service(get);
} }
struct ItemClassListEntry {
id: Uuid,
name: String,
parent: Option<ItemClassPreview>,
}
#[get("/item-classes")] #[get("/item-classes")]
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> { async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at") let item_classes = query!(
.fetch_all(pool.as_ref()) r#"SELECT class.id, class.name, class.parent, parent.name AS "parent_name?"
.await FROM item_classes AS "class"
.map_err(error::ErrorInternalServerError)?; LEFT JOIN item_classes AS "parent"
ON class.parent = parent.id
let item_classes = manage::item_class::get_all_as_map(&pool) ORDER BY class.created_at
.await "#
.map_err(error::ErrorInternalServerError)?; )
.map(|row| ItemClassListEntry {
id: row.id,
name: row.name,
parent: row
.parent
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
})
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
Ok(templates::base( Ok(templates::base(
TemplateConfig { TemplateConfig {
@ -56,14 +72,12 @@ async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
} }
} }
tbody { tbody {
@for item_class in item_classes_ids { @for item_class in item_classes {
@let item_class = item_classes.get(&item_class).unwrap();
tr { tr {
td { a href={ "/item-class/" (item_class.id) } { (item_class.name) } } td { (ItemClassPreview::new(item_class.id, item_class.name)) }
td { td {
@if let Some(parent) = item_class.parent { @if let Some(parent) = item_class.parent {
@let parent = item_classes.get(&parent).unwrap(); (parent)
a href={ "/item-class/" (parent.id) } { (parent.name) }
} @else { } @else {
"-" "-"
} }

View file

@ -5,20 +5,28 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::{query, query_as, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::frontend::templates::{ use crate::frontend::templates::{
self, self,
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod}, helpers::{
Colour, ItemClassPreview, ItemPreview, PageAction, PageActionGroup, PageActionMethod,
},
TemplateConfig, TemplateConfig,
}; };
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get); cfg.service(get);
} }
struct ItemClassDetails {
id: Uuid,
name: String,
description: String,
parent: Option<ItemClassPreview>,
}
#[get("/item-class/{id}")] #[get("/item-class/{id}")]
async fn get( async fn get(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
@ -27,26 +35,52 @@ async fn get(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
let item_class = manage::item_class::get(&pool, id) let item_class = query!(
.await r#"SELECT
.map_err(error::ErrorInternalServerError)?; class.id,
class.name,
class.description,
class.parent,
parent.name AS "parent_name?"
FROM item_classes AS "class"
LEFT JOIN item_classes AS "parent"
ON class.parent = parent.id
WHERE class.id = $1"#,
id
)
.map(|row| ItemClassDetails {
id: row.id,
name: row.name,
description: row.description,
parent: row
.parent
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
})
.fetch_one(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
// TODO: Once async closures are stable, use map_or on item_class.parent instead let children = query_as!(
let parent = match item_class.parent { ItemClassPreview,
Some(id) => manage::item_class::get(&pool, id) "SELECT id, name FROM item_classes WHERE parent = $1",
.await id
.map(Some) )
.map_err(error::ErrorInternalServerError)?, .fetch_all(pool.as_ref())
None => None, .await
}; .map_err(error::ErrorInternalServerError)?;
let children = manage::item_class::children(&pool, id) let items = query!(
.await r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map_err(error::ErrorInternalServerError)?; FROM items
JOIN item_classes
let items = manage::item_class::items(&pool, id) ON items.class = item_classes.id
.await WHERE items.class = $1"#,
.map_err(error::ErrorInternalServerError)?; id
)
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let mut title = item_class.name.clone(); let mut title = item_class.name.clone();
title.push_str(" Item Details"); title.push_str(" Item Details");
@ -107,10 +141,10 @@ async fn get(
th { "Name" } th { "Name" }
td { (item_class.name) } td { (item_class.name) }
} }
@if let Some(parent) = parent { @if let Some(parent) = item_class.parent {
tr { tr {
th { "Parent" } th { "Parent" }
td { a href={ "/item-class/" (parent.id) } { (parent.name) } } td { (parent) }
} }
} }
tr { tr {
@ -124,9 +158,7 @@ async fn get(
ul { ul {
@for child in children { @for child in children {
li { li { (child) }
a href={ "/item-class/" (child.id) } { (child.name) }
}
} }
} }
} }
@ -145,9 +177,7 @@ async fn get(
ul { ul {
@for item in items { @for item in items {
li { li { (item) }
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &item_class.name).terse()))
}
} }
} }
} }

View file

@ -5,16 +5,19 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::{query, query_scalar, PgPool};
use uuid::Uuid; use uuid::Uuid;
use crate::manage;
use crate::models::EntityType;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(get); cfg.service(get);
} }
#[derive(Deserialize)]
pub enum EntityType {
Item,
ItemClass,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct JumpData { struct JumpData {
id: String, id: String,
@ -29,15 +32,29 @@ async fn get(
let mut id = data.id.clone(); let mut id = data.id.clone();
let entity_type = if let Ok(id) = Uuid::parse_str(&id) { let entity_type = if let Ok(id) = Uuid::parse_str(&id) {
manage::query_entity_type(&pool, id) query!(
.await r#"SELECT type as "type!"
.map_err(error::ErrorInternalServerError)? FROM (SELECT id, 'item' AS "type" FROM items
UNION ALL
SELECT id, 'item_class' AS "type" FROM item_classes) id_mapping
WHERE id = $1"#,
id
)
.map(|row| match row.r#type.as_str() {
"item" => EntityType::Item,
"item_class" => EntityType::ItemClass,
_ => unreachable!("database returned impossible type"),
})
.fetch_optional(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?
} else if let Ok(short_id) = id.parse::<i32>() { } else if let Ok(short_id) = id.parse::<i32>() {
if let Ok(item) = manage::item::get_by_short_id(&pool, short_id) if let Ok(id_) = query_scalar!("SELECT id FROM items WHERE short_id = $1", short_id)
.fetch_one(pool.as_ref())
.await .await
.map_err(error::ErrorInternalServerError) .map_err(error::ErrorInternalServerError)
{ {
id = item.id.to_string(); id = id_.to_string();
Some(EntityType::Item) Some(EntityType::Item)
} else { } else {
None None

View file

@ -7,12 +7,12 @@ use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
use serde_variant::to_variant_name; use serde_variant::to_variant_name;
use sqlx::PgPool; use sqlx::{query, PgPool};
use uuid::Uuid; use uuid::Uuid;
use super::templates::{self, datalist, helpers::ItemName, TemplateConfig}; use super::templates::{self, datalist, TemplateConfig};
use crate::frontend::templates::helpers::ItemPreview;
use crate::label::{Label, LabelPreset}; use crate::label::{Label, LabelPreset};
use crate::manage;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(generate_post) cfg.service(generate_post)
@ -35,11 +35,9 @@ async fn generate(pool: &PgPool, params: GenerateParams) -> actix_web::Result<im
.collect::<Result<Vec<Uuid>, uuid::Error>>() .collect::<Result<Vec<Uuid>, uuid::Error>>()
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
let items = manage::item::get_multiple(pool, &ids) Label::for_items(pool, &ids, params.preset.clone().into())
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)
Ok(Label::for_items(&items, params.preset.clone().into()))
} }
#[post("/labels/generate")] #[post("/labels/generate")]
@ -62,13 +60,16 @@ async fn generate_get(
#[get("/labels")] #[get("/labels")]
async fn form(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> { async fn form(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
let items = manage::item::get_all(&pool) let items = query!(
.await r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map_err(error::ErrorInternalServerError)?; FROM items
JOIN item_classes
let item_classes = manage::item_class::get_all_as_map(&pool) ON items.class = item_classes.id"#
.await )
.map_err(error::ErrorInternalServerError)?; .map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
.fetch_all(pool.as_ref())
.await
.map_err(error::ErrorInternalServerError)?;
let datalist_items = datalist::items(&pool) let datalist_items = datalist::items(&pool)
.await .await
@ -120,8 +121,7 @@ async fn form(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl
label .form-label for="ids-multiselect" { "Items" } label .form-label for="ids-multiselect" { "Items" }
select #ids-multiselect .form-select multiple size="15" { select #ids-multiselect .form-select multiple size="15" {
@for item in items { @for item in items {
@let item_name = ItemName::new(item.name.as_ref(), &item_classes.get(&item.class).unwrap().name); option value={ (item.id) } { (item.name) " (" (item.id) ")" }
option value={ (item.id) } { (item_name.to_string()) " (" (item.id) ")" }
} }
} }
} }

View file

@ -3,10 +3,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use maud::{html, Markup, Render}; use maud::{html, Markup, Render};
use sqlx::PgPool; use sqlx::{query, PgPool};
use super::helpers::ItemName; use super::helpers::ItemName;
use crate::manage;
pub struct Datalist { pub struct Datalist {
name: String, name: String,
@ -44,23 +43,23 @@ impl Render for DatalistOption {
} }
pub async fn items(pool: &PgPool) -> Result<Datalist, sqlx::Error> { pub async fn items(pool: &PgPool) -> Result<Datalist, sqlx::Error> {
let items = manage::item::get_all(pool).await?;
let item_classes = manage::item_class::get_all_as_map(pool).await?;
Ok(Datalist { Ok(Datalist {
name: "items".to_string(), name: "items".to_string(),
link_prefix: Some("/item/".to_string()), link_prefix: Some("/item/".to_string()),
options: items options: query!(
.iter() r#"SELECT items.id, items.name, item_classes.name AS "class_name"
.map(|i| DatalistOption { FROM items
value: i.id.to_string(), JOIN item_classes
text: Box::new(ItemName::new( ON items.class = item_classes.id"#
i.name.as_ref(), )
&item_classes.get(&i.class).unwrap().name, .fetch_all(pool)
)), .await?
}) .into_iter()
.collect(), .map(|row| DatalistOption {
value: row.id.to_string(),
text: Box::new(ItemName::new(row.name.as_ref(), &row.class_name)),
})
.collect(),
}) })
} }
@ -68,12 +67,13 @@ pub async fn item_classes(pool: &PgPool) -> Result<Datalist, sqlx::Error> {
Ok(Datalist { Ok(Datalist {
name: "item-classes".to_string(), name: "item-classes".to_string(),
link_prefix: Some("/item-class/".to_string()), link_prefix: Some("/item-class/".to_string()),
options: manage::item_class::get_all(pool) options: query!("SELECT id, name FROM item_classes")
.fetch_all(pool)
.await? .await?
.into_iter() .into_iter()
.map(|ic| DatalistOption { .map(|row| DatalistOption {
value: ic.id.to_string(), value: row.id.to_string(),
text: Box::new(ic.name), text: Box::new(row.name),
}) })
.collect(), .collect(),
}) })

View file

@ -157,6 +157,25 @@ impl Render for ItemPreview {
} }
} }
pub struct ItemClassPreview {
pub id: Uuid,
pub name: String,
}
impl ItemClassPreview {
pub fn new(id: Uuid, name: String) -> Self {
Self { id, name }
}
}
impl Render for ItemClassPreview {
fn render(&self) -> Markup {
html! {
a href={ "/item-class/" (self.id) } { (self.name) }
}
}
}
pub enum PageActionMethod { pub enum PageActionMethod {
Get, Get,
Post, Post,

View file

@ -12,10 +12,10 @@ use barcode::{encode_code128, encode_data_matrix};
use pdf::{IndirectFontRef, PdfLayerReference}; use pdf::{IndirectFontRef, PdfLayerReference};
use printpdf as pdf; use printpdf as pdf;
use printpdf::{ImageTransform, Mm, PdfDocument, PdfDocumentReference, Pt, Px}; use printpdf::{ImageTransform, Mm, PdfDocument, PdfDocumentReference, Pt, Px};
use sqlx::{query_as, PgPool};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::models::Item;
pub use preset::LabelPreset; pub use preset::LabelPreset;
const FONT: Cursor<&[u8]> = Cursor::new(include_bytes!( const FONT: Cursor<&[u8]> = Cursor::new(include_bytes!(
@ -247,16 +247,18 @@ impl Label {
Ok(doc.ok_or(Error::NoPages)?.save_to_bytes()?) Ok(doc.ok_or(Error::NoPages)?.save_to_bytes()?)
} }
pub fn for_items(items: &[Item], config: LabelConfig) -> Self { pub async fn for_items(pool: &PgPool, ids: &[Uuid], config: LabelConfig) -> sqlx::Result<Self> {
Label { Ok(Label {
pages: items pages: query_as!(
.iter() LabelPage,
.map(|item| LabelPage { r#"SELECT id AS "id?", to_char(short_id, '000000') AS "short_id?"
id: Some(item.id), FROM items
short_id: Some(format!("{:06}", item.short_id)), WHERE id = ANY ($1)"#,
}) ids
.collect(), )
.fetch_all(pool)
.await?,
config, config,
} })
} }
} }

View file

@ -4,6 +4,4 @@
pub mod frontend; pub mod frontend;
pub mod label; pub mod label;
pub mod manage;
pub mod middleware; pub mod middleware;
pub mod models;

View file

@ -1,155 +0,0 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use sqlx::{query, query_as, query_scalar, PgPool};
use uuid::Uuid;
use crate::models::{Item, NewItem};
pub async fn add(pool: &PgPool, new_item: NewItem) -> Result<Item, sqlx::Error> {
query_as!(
Item,
"INSERT INTO items (name, parent, class, original_packaging, description) VALUES ($1, $2, $3, $4, $5) RETURNING *",
new_item.name,
new_item.parent,
new_item.class,
new_item.original_packaging,
new_item.description
)
.fetch_one(pool)
.await
}
pub async fn add_multiple(
pool: &PgPool,
new_item: NewItem,
quantity: usize,
) -> Result<Vec<Item>, sqlx::Error> {
query_as!(
Item,
r#"INSERT INTO items (name, parent, class, original_packaging, description)
SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])
RETURNING *"#,
&vec![new_item.name; quantity] as &[Option<String>],
&vec![new_item.parent; quantity] as &[Option<Uuid>],
&vec![new_item.class; quantity],
&vec![new_item.original_packaging; quantity] as &[Option<Uuid>],
&vec![new_item.description; quantity]
)
.fetch_all(pool)
.await
}
pub async fn get(pool: &PgPool, id: Uuid) -> Result<Item, sqlx::Error> {
query_as!(Item, "SELECT * FROM items WHERE id = $1", id)
.fetch_one(pool)
.await
}
pub async fn get_by_short_id(pool: &PgPool, short_id: i32) -> Result<Item, sqlx::Error> {
query_as!(Item, "SELECT * FROM items WHERE short_id = $1", short_id)
.fetch_one(pool)
.await
}
pub async fn get_all(pool: &PgPool) -> Result<Vec<Item>, sqlx::Error> {
query_as!(Item, "SELECT * FROM items ORDER BY created_at")
.fetch_all(pool)
.await
}
pub async fn get_multiple(pool: &PgPool, ids: &[Uuid]) -> Result<Vec<Item>, sqlx::Error> {
query_as!(Item, "SELECT * FROM items WHERE id = ANY ($1)", ids)
.fetch_all(pool)
.await
}
pub async fn get_all_as_map(pool: &PgPool) -> Result<HashMap<Uuid, Item>, sqlx::Error> {
Ok(get_all(pool)
.await?
.into_iter()
.map(|i| (i.id, i))
.collect())
}
pub async fn update(pool: &PgPool, id: Uuid, modified_item: NewItem) -> Result<Item, sqlx::Error> {
query_as!(
Item,
"UPDATE items SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6 WHERE id = $1 RETURNING *",
id,
modified_item.name,
modified_item.parent,
modified_item.class,
modified_item.original_packaging,
modified_item.description
)
.fetch_one(pool)
.await
}
pub async fn delete(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
let res = query!("DELETE FROM items WHERE id = $1", id)
.execute(pool)
.await?;
assert_eq!(res.rows_affected(), 1);
Ok(())
}
pub async fn get_parents(pool: &PgPool, id: Uuid) -> Result<Vec<Uuid>, sqlx::Error> {
// force nullable is required for all columns in views
query_scalar!(
r#"SELECT unnest(parents) as "parents!" FROM item_tree WHERE id = $1"#,
id
)
.fetch_all(pool)
.await
}
pub async fn get_all_parents(pool: &PgPool) -> Result<HashMap<Uuid, Vec<Uuid>>, sqlx::Error> {
let mut parents = HashMap::new();
for row in query!(r#"SELECT id as "id!", parents as "parents!" FROM item_tree"#)
.fetch_all(pool)
.await?
{
parents.insert(row.id, row.parents);
}
Ok(parents)
}
pub async fn get_parents_details(pool: &PgPool, id: Uuid) -> Result<Vec<Item>, sqlx::Error> {
query_as!(
Item,
"SELECT items.*
FROM items
INNER JOIN
unnest((SELECT parents FROM item_tree WHERE id = $1))
WITH ORDINALITY AS parents(id, n)
ON items.id = parents.id
ORDER BY parents.n;",
id
)
.fetch_all(pool)
.await
}
pub async fn get_children(pool: &PgPool, id: Uuid) -> Result<Vec<Item>, sqlx::Error> {
query_as!(Item, "SELECT * FROM items WHERE parent = $1", id)
.fetch_all(pool)
.await
}
pub async fn original_packaging_contents(
pool: &PgPool,
id: Uuid,
) -> Result<Vec<Item>, sqlx::Error> {
query_as!(
Item,
"SELECT * FROM items WHERE original_packaging = $1",
id
)
.fetch_all(pool)
.await
}

View file

@ -1,83 +0,0 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use sqlx::{query, query_as, PgPool};
use uuid::Uuid;
use crate::models::{Item, ItemClass, NewItemClass};
pub async fn add(pool: &PgPool, new_item_class: NewItemClass) -> Result<ItemClass, sqlx::Error> {
query_as!(
ItemClass,
"INSERT INTO item_classes (name, parent, description) VALUES ($1, $2, $3) RETURNING *",
new_item_class.name,
new_item_class.parent,
new_item_class.description
)
.fetch_one(pool)
.await
}
pub async fn get(pool: &PgPool, id: Uuid) -> Result<ItemClass, sqlx::Error> {
query_as!(ItemClass, "SELECT * FROM item_classes WHERE id = $1", id)
.fetch_one(pool)
.await
}
pub async fn get_all(pool: &PgPool) -> Result<Vec<ItemClass>, sqlx::Error> {
query_as!(ItemClass, "SELECT * FROM item_classes ORDER BY created_at")
.fetch_all(pool)
.await
}
pub async fn get_all_as_map(pool: &PgPool) -> Result<HashMap<Uuid, ItemClass>, sqlx::Error> {
Ok(get_all(pool)
.await?
.into_iter()
.map(|ic| (ic.id, ic))
.collect())
}
pub async fn update(
pool: &PgPool,
id: Uuid,
modified_item_class: NewItemClass,
) -> Result<ItemClass, sqlx::Error> {
query_as!(
ItemClass,
"UPDATE item_classes SET name = $2, parent = $3, description = $4 WHERE id = $1 RETURNING *",
id,
modified_item_class.name,
modified_item_class.parent,
modified_item_class.description
)
.fetch_one(pool)
.await
}
pub async fn items(pool: &PgPool, id: Uuid) -> Result<Vec<Item>, sqlx::Error> {
query_as!(Item, "SELECT * FROM items WHERE class = $1", id)
.fetch_all(pool)
.await
}
pub async fn delete(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
let res = query!("DELETE FROM item_classes WHERE id = $1", id)
.execute(pool)
.await?;
assert_eq!(res.rows_affected(), 1);
Ok(())
}
pub async fn children(pool: &PgPool, id: Uuid) -> Result<Vec<ItemClass>, sqlx::Error> {
query_as!(
ItemClass,
"SELECT * FROM item_classes WHERE parent = $1",
id
)
.fetch_all(pool)
.await
}

View file

@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod item;
pub mod item_class;
use sqlx::{query, PgPool};
use uuid::Uuid;
use crate::models::EntityType;
pub async fn query_entity_type(pool: &PgPool, id: Uuid) -> Result<Option<EntityType>, sqlx::Error> {
Ok(query!(
r#"SELECT type as "type!" FROM
(SELECT id, 'item' AS "type" FROM items
UNION ALL
SELECT id, 'item_class' AS "type" FROM item_classes) id_mapping
WHERE id = $1"#,
id
)
.fetch_optional(pool)
.await?
.map(|row| match row.r#type.as_str() {
"item" => EntityType::Item,
"item_class" => EntityType::ItemClass,
_ => unreachable!("database returned impossible type"),
}))
}

View file

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Deserialize)]
pub enum EntityType {
Item,
ItemClass,
}
#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
pub struct Item {
pub id: Uuid,
pub name: Option<String>,
pub parent: Option<Uuid>,
pub class: Uuid,
#[serde(with = "time::serde::iso8601")]
pub created_at: OffsetDateTime,
pub short_id: i32,
pub original_packaging: Option<Uuid>,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct NewItem {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub parent: Option<Uuid>,
pub class: Uuid,
pub original_packaging: Option<Uuid>,
pub description: String,
}
#[derive(Clone, Debug, Serialize)]
pub struct ItemClass {
pub id: Uuid,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<Uuid>,
#[serde(with = "time::serde::iso8601")]
pub created_at: OffsetDateTime,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct NewItemClass {
pub name: String,
#[serde(default)]
pub parent: Option<Uuid>,
pub description: String,
}