This commit is contained in:
parent
fb3706ff38
commit
6a3ce7e9d3
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM settings_blocks WHERE id = $1 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM data_points WHERE chart_block_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527"
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO table_columns (table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\")\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Int4",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681"
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "chart_block_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "x",
|
||||
"type_info": "Float8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "y",
|
||||
"type_info": "Float8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263"
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE slug LIKE '%-settings' AND is_active = true\n ORDER BY title\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7"
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM hero_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "subtitle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "background_image",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "background_color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "text_color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "cta_text",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "cta_link",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM hero_blocks WHERE id = $1 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM text_blocks WHERE page_id = $1 AND is_active = true",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c"
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE text_blocks \n SET title = COALESCE($1, title),\n markdown = COALESCE($2, markdown),\n block_order = COALESCE($3, block_order),\n is_active = COALESCE($4, is_active),\n updated_at = NOW()\n WHERE id = $5\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "markdown",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6"
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE chart_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "table_block_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "data_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_sortable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_filterable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "width",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "order",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0"
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n COUNT(*) as total_count,\n COUNT(CASE WHEN is_system = true THEN 1 END) as system_count,\n COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count\n FROM settings \n WHERE category = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "total_count",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "system_count",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "editable_count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM hero_blocks WHERE page_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM hero_blocks WHERE page_id = $1 AND is_active = true",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM settings_blocks WHERE page_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, \"order\"\n FROM table_columns WHERE table_block_id = $1 ORDER BY \"order\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "table_block_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "data_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_sortable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_filterable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "width",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "order",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8"
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE text_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM table_blocks WHERE id = $1 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM table_blocks WHERE page_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d"
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO settings_blocks (page_id, block_order, title, category, editable, display_mode, is_active)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "editable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "display_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b"
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE hero_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3"
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO chart_blocks (page_id, block_order, title, chart_type, config, is_active)\n VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "chart_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM settings_blocks WHERE page_id = $1 AND is_active = true",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe"
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Float8",
|
||||
"Float8",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE id = $1 AND is_active = true\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM pages WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM text_blocks WHERE id = $1 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM chart_blocks WHERE page_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922"
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE hero_blocks \n SET title = COALESCE($1, title),\n subtitle = COALESCE($2, subtitle),\n background_image = COALESCE($3, background_image),\n background_color = COALESCE($4, background_color),\n text_color = COALESCE($5, text_color),\n cta_text = COALESCE($6, cta_text),\n cta_link = COALESCE($7, cta_link),\n block_order = COALESCE($8, block_order),\n is_active = COALESCE($9, is_active),\n updated_at = NOW()\n WHERE id = $10\n RETURNING id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "subtitle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "background_image",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "background_color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "text_color",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "cta_text",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "cta_link",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc"
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE table_blocks \n SET title = COALESCE($1, title),\n data_source = COALESCE($2, data_source),\n data_config = COALESCE($3, data_config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, data_source, data_config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "data_source",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "data_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM chart_blocks WHERE id = $1 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM chart_blocks WHERE page_id = $1 AND is_active = true",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea"
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE pages \n SET title = COALESCE($1, title),\n slug = COALESCE($2, slug),\n description = COALESCE($3, description),\n is_active = COALESCE($4, is_active),\n updated_at = NOW(),\n updated_by = $5\n WHERE id = $6\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad"
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO pages (title, slug, description, is_active, created_by)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f"
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages \n WHERE is_active = true \n AND (title ILIKE $1 OR description ILIKE $1 OR slug ILIKE $1)\n ORDER BY \n CASE \n WHEN title ILIKE $1 THEN 1\n WHEN slug ILIKE $1 THEN 2\n ELSE 3\n END,\n updated_at DESC\n LIMIT $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM data_points WHERE chart_block_id IN (SELECT id FROM chart_blocks WHERE page_id = $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM table_blocks WHERE page_id = $1 AND is_active = true",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb"
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE table_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da"
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO data_points (chart_block_id, x, y, label, color)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Float8",
|
||||
"Float8",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa"
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, chart_block_id, x, y, label, color\n FROM data_points WHERE chart_block_id = $1 ORDER BY x\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "chart_block_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "x",
|
||||
"type_info": "Float8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "y",
|
||||
"type_info": "Float8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d"
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE settings_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM chart_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "chart_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM table_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "data_source",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "data_config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28"
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\", \n created_by, updated_by\n FROM pages WHERE slug = $1 AND is_active = true\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_by",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_by",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7"
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE settings_blocks \n SET title = COALESCE($1, title),\n category = COALESCE($2, category),\n editable = COALESCE($3, editable),\n display_mode = COALESCE($4, display_mode),\n block_order = COALESCE($5, block_order),\n is_active = COALESCE($6, is_active),\n updated_at = NOW()\n WHERE id = $7\n RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "editable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "display_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM table_columns WHERE table_block_id IN (SELECT id FROM table_blocks WHERE page_id = $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM text_blocks WHERE page_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de"
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM settings_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "category",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "editable",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "display_mode",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994"
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM table_columns WHERE table_block_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13"
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) as count FROM pages WHERE slug = $1 AND id != $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad"
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO text_blocks (page_id, block_order, title, markdown, is_active)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, page_id, block_order, title, markdown, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "markdown",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1"
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE chart_blocks \n SET title = COALESCE($1, title),\n chart_type = COALESCE($2, chart_type),\n config = COALESCE($3, config),\n block_order = COALESCE($4, block_order),\n is_active = COALESCE($5, is_active),\n updated_at = NOW()\n WHERE id = $6\n RETURNING id, page_id, block_order, title, chart_type, config, is_active, \n created_at as \"created_at: DateTime<Utc>\", \n updated_at as \"updated_at: DateTime<Utc>\"\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "chart_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "config",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5"
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, page_id, block_order, title, markdown, is_active, created_at as \"created_at: DateTime<Utc>\", updated_at as \"updated_at: DateTime<Utc>\"\n FROM text_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "page_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "block_order",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "title",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "markdown",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_active",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "created_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a"
|
||||
}
|
||||
132
migrations/014_insert_default_settings.sql
Normal file
132
migrations/014_insert_default_settings.sql
Normal file
@ -0,0 +1,132 @@
|
||||
-- Rollback: Delete default configuration items
|
||||
-- Delete all inserted configuration items
|
||||
DELETE FROM settings WHERE key IN (
|
||||
-- Site configuration
|
||||
'site.name',
|
||||
'site.description',
|
||||
'site.keywords',
|
||||
'site.url',
|
||||
'site.logo',
|
||||
'site.copyright',
|
||||
'site.icp',
|
||||
'site.icp_url',
|
||||
'site.color_style',
|
||||
|
||||
-- User configuration
|
||||
'user.default_avatar',
|
||||
'user.default_role',
|
||||
'user.register_invite_code',
|
||||
'user.register_email_verification',
|
||||
'user.open_login',
|
||||
'user.open_reset_password',
|
||||
|
||||
-- Email configuration
|
||||
'email.smtp_host',
|
||||
'email.smtp_port',
|
||||
'email.smtp_user',
|
||||
'email.smtp_password',
|
||||
'email.smtp_from',
|
||||
'email.smtp_from_name',
|
||||
'email.smtp_from_email',
|
||||
'email.system_template',
|
||||
|
||||
-- Blog configuration
|
||||
'blog.default_author',
|
||||
'blog.default_category',
|
||||
'blog.default_tag',
|
||||
'blog.open_comment',
|
||||
|
||||
-- Logging configuration
|
||||
'logging.level',
|
||||
'logging.max_files',
|
||||
'logging.max_file_size',
|
||||
|
||||
-- Cache configuration
|
||||
'cache.ttl',
|
||||
'cache.max_size',
|
||||
|
||||
-- Feature switches configuration
|
||||
'switch.open_register',
|
||||
'switch.open_login',
|
||||
'switch.open_reset_password',
|
||||
'switch.open_comment',
|
||||
'switch.open_like',
|
||||
'switch.open_share',
|
||||
'switch.open_view'
|
||||
);
|
||||
|
||||
-- Drop unique index (if exists)
|
||||
DROP INDEX IF EXISTS idx_settings_key_unique;
|
||||
|
||||
-- Insert default configuration items
|
||||
-- Site configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('site.name', 'Mapp', 'string', 'Site name', 'site', true, true),
|
||||
('site.description', 'A modern application platform', 'string', 'Site description', 'site', true, true),
|
||||
('site.keywords', 'mapp,application,platform', 'string', 'Site keywords', 'site', true, true),
|
||||
('site.url', 'http://localhost:3000', 'string', 'Site URL', 'site', true, true),
|
||||
('site.logo', '/static/logo.png', 'string', 'Site logo path', 'site', true, true),
|
||||
('site.copyright', '© 2024 Mapp. All rights reserved.', 'string', 'Copyright information', 'site', true, true),
|
||||
('site.icp', '', 'string', 'ICP registration number', 'site', true, true),
|
||||
('site.icp_url', '', 'string', 'ICP registration URL', 'site', true, true),
|
||||
('site.color_style', 'default', 'string', 'Site color scheme', 'site', true, true);
|
||||
|
||||
-- User configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('user.default_avatar', '/static/default-avatar.png', 'string', 'Default user avatar', 'user', true, true),
|
||||
('user.default_role', 'User', 'string', 'Default user role', 'user', true, true),
|
||||
('user.register_invite_code', 'true', 'boolean', 'Require invite code for registration', 'user', true, true),
|
||||
('user.register_email_verification', 'false', 'boolean', 'Require email verification for registration', 'user', true, true),
|
||||
('user.open_login', 'true', 'boolean', 'Enable login', 'user', true, true),
|
||||
('user.open_reset_password', 'true', 'boolean', 'Enable password reset', 'user', true, true);
|
||||
|
||||
-- Email configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('email.smtp_host', '', 'string', 'SMTP server address', 'email', true, true),
|
||||
('email.smtp_port', '587', 'number', 'SMTP server port', 'email', true, true),
|
||||
('email.smtp_user', '', 'string', 'SMTP username', 'email', true, true),
|
||||
('email.smtp_password', '', 'string', 'SMTP password', 'email', true, true),
|
||||
('email.smtp_from', '', 'string', 'Sender email', 'email', true, true),
|
||||
('email.smtp_from_name', 'Mapp System', 'string', 'Sender name', 'email', true, true),
|
||||
('email.smtp_from_email', '', 'string', 'Sender email address', 'email', true, true),
|
||||
('email.system_template', 'default', 'string', 'System email template', 'email', true, true);
|
||||
|
||||
-- Blog configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('blog.default_author', 'System', 'string', 'Default blog author', 'blog', true, true),
|
||||
('blog.default_category', 'Uncategorized', 'string', 'Default blog category', 'blog', true, true),
|
||||
('blog.default_tag', 'Default', 'string', 'Default blog tag', 'blog', true, true),
|
||||
('blog.open_comment', 'true', 'boolean', 'Enable comments', 'blog', true, true);
|
||||
|
||||
-- Logging configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('logging.level', 'info', 'string', 'Log level', 'logging', true, true),
|
||||
('logging.max_files', '10', 'number', 'Maximum log files', 'logging', true, true),
|
||||
('logging.max_file_size', '10485760', 'number', 'Maximum log file size (bytes)', 'logging', true, true);
|
||||
|
||||
-- Cache configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('cache.ttl', '3600', 'number', 'Cache time to live (seconds)', 'cache', true, true),
|
||||
('cache.max_size', '1000', 'number', 'Maximum cache entries', 'cache', true, true);
|
||||
|
||||
-- Feature switches configuration
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_system, is_editable) VALUES
|
||||
('switch.open_register', 'true', 'boolean', 'Enable registration', 'switch', true, true),
|
||||
('switch.open_login', 'true', 'boolean', 'Enable login', 'switch', true, true),
|
||||
('switch.open_reset_password', 'true', 'boolean', 'Enable password reset', 'switch', true, true),
|
||||
('switch.open_comment', 'true', 'boolean', 'Enable comments', 'switch', true, true),
|
||||
('switch.open_like', 'true', 'boolean', 'Enable likes', 'switch', true, true),
|
||||
('switch.open_share', 'true', 'boolean', 'Enable sharing', 'switch', true, true),
|
||||
('switch.open_view', 'true', 'boolean', 'Enable view statistics', 'switch', true, true);
|
||||
|
||||
-- 创建唯一索引约束(如果不存在)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE indexname = 'idx_settings_key_unique'
|
||||
AND tablename = 'settings'
|
||||
) THEN
|
||||
CREATE UNIQUE INDEX idx_settings_key_unique ON settings(key);
|
||||
END IF;
|
||||
END $$;
|
||||
82
src/app.rs
82
src/app.rs
@ -10,7 +10,6 @@ use async_graphql::{
|
||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
|
||||
use axum::{
|
||||
extract::{FromRef, State},
|
||||
http::Method,
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
Router,
|
||||
@ -24,10 +23,10 @@ use crate::{
|
||||
config::Config,
|
||||
graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot},
|
||||
services::{
|
||||
blog_service::BlogService, casbin_service::CasbinService,
|
||||
invite_code_service::InviteCodeService, mosaic_service::MosaicService,
|
||||
page_block_service::PageBlockService, settings_service::SettingsService,
|
||||
system_config_service::SystemConfigService, user_service::UserService,
|
||||
blog_service::BlogService, casbin_service::CasbinService, config_manager::ConfigsManager,
|
||||
config_service::ConfigsService, invite_code_service::InviteCodeService,
|
||||
mosaic_service::MosaicService, system_config_service::SystemConfigService,
|
||||
user_service::UserService,
|
||||
},
|
||||
};
|
||||
|
||||
@ -48,31 +47,36 @@ pub async fn create_router(
|
||||
config: Config,
|
||||
status_sender: Option<broadcast::Sender<StatusUpdate>>,
|
||||
) -> Router {
|
||||
let user_service = UserService::new(pool.clone(), config.jwt_secret.clone());
|
||||
let invite_code_service = InviteCodeService::new(pool.clone());
|
||||
let system_config_service = SystemConfigService::new(pool.clone());
|
||||
let mosaic_service = MosaicService::new(pool.clone());
|
||||
let settings_service = SettingsService::new(pool.clone());
|
||||
let page_block_service = PageBlockService::new(pool.clone());
|
||||
let blog_service = BlogService::new(pool.clone());
|
||||
|
||||
let casbin_service = CasbinService::new(config.database_url.clone())
|
||||
.await
|
||||
.expect("Failed to initialize CasbinService");
|
||||
|
||||
let schema = Schema::build(QueryRoot, MutationRoot, SubscriptionRoot)
|
||||
.data(pool)
|
||||
.data(user_service)
|
||||
.data(invite_code_service)
|
||||
.data(system_config_service)
|
||||
.data(mosaic_service)
|
||||
.data(settings_service)
|
||||
.data(page_block_service)
|
||||
.data(blog_service)
|
||||
.data(casbin_service)
|
||||
.data(config.clone())
|
||||
.data(status_sender.clone())
|
||||
.finish();
|
||||
let user_service = UserService::new(pool.clone(), config.jwt_secret.clone())
|
||||
.with_casbin(casbin_service.clone());
|
||||
|
||||
let invite_code_service = InviteCodeService::new(pool.clone());
|
||||
let system_config_service = SystemConfigService::new(pool.clone());
|
||||
let mosaic_service = MosaicService::new(pool.clone());
|
||||
let configs_service = ConfigsService::new(pool.clone());
|
||||
let config_manager = ConfigsManager::new(configs_service).await;
|
||||
let blog_service = BlogService::new(pool.clone());
|
||||
|
||||
let schema = Schema::build(
|
||||
QueryRoot::default(),
|
||||
MutationRoot::default(),
|
||||
SubscriptionRoot,
|
||||
)
|
||||
.data(pool)
|
||||
.data(user_service)
|
||||
.data(invite_code_service)
|
||||
.data(system_config_service)
|
||||
.data(mosaic_service)
|
||||
.data(config_manager)
|
||||
.data(blog_service)
|
||||
.data(casbin_service)
|
||||
.data(config.clone())
|
||||
.data(status_sender.clone())
|
||||
.finish();
|
||||
|
||||
let keys = vec![DecodingKey::from_secret(config.jwt_secret.as_bytes())];
|
||||
let validation = Validation::default();
|
||||
@ -94,19 +98,19 @@ pub async fn create_router(
|
||||
Router::new()
|
||||
.route("/", get(graphql_playground))
|
||||
.route("/graphql", get(graphql_playground).post(graphql_handler))
|
||||
.route_layer(
|
||||
RateLimitLayer::<RealIp>::builder()
|
||||
.with_route(
|
||||
(Method::GET, "/graphql"),
|
||||
Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
|
||||
)
|
||||
.with_route(
|
||||
(Method::POST, "/graphql"),
|
||||
Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
|
||||
)
|
||||
.with_gc_interval(1000)
|
||||
.default_handle_error(),
|
||||
)
|
||||
// .route_layer(
|
||||
// RateLimitLayer::<RealIp>::builder()
|
||||
// .with_route(
|
||||
// (Method::GET, "/graphql"),
|
||||
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
|
||||
// )
|
||||
// .with_route(
|
||||
// (Method::POST, "/graphql"),
|
||||
// Quota::new(Duration::from_millis(100), NonZero::new(10).unwrap()),
|
||||
// )
|
||||
// .with_gc_interval(1000)
|
||||
// .default_handle_error(),
|
||||
// )
|
||||
.route_service("/ws", GraphQLSubscription::new(schema))
|
||||
.layer(CorsLayer::permissive())
|
||||
.merge(router)
|
||||
|
||||
@ -3,6 +3,15 @@ use crate::{auth::get_auth_user, services::casbin_service::CasbinService};
|
||||
use async_graphql::{Context, Error, Guard, Result};
|
||||
use tracing::warn;
|
||||
|
||||
pub struct RequireLogin;
|
||||
|
||||
impl Guard for RequireLogin {
|
||||
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
|
||||
get_auth_user(ctx).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RequireRole(pub Role);
|
||||
|
||||
impl Guard for RequireRole {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
pub mod guards;
|
||||
pub mod mutation;
|
||||
pub mod query;
|
||||
pub mod mutations;
|
||||
pub mod queries;
|
||||
pub mod subscription;
|
||||
pub mod types;
|
||||
|
||||
pub use mutation::MutationRoot;
|
||||
pub use query::QueryRoot;
|
||||
pub use mutations::MutationRoot;
|
||||
pub use queries::QueryRoot;
|
||||
pub use subscription::SubscriptionRoot;
|
||||
File diff suppressed because it is too large
Load Diff
13
src/graphql/mutations.rs
Normal file
13
src/graphql/mutations.rs
Normal file
@ -0,0 +1,13 @@
|
||||
mod blog;
|
||||
mod config;
|
||||
mod permissions;
|
||||
mod users;
|
||||
use async_graphql::MergedObject;
|
||||
|
||||
#[derive(MergedObject, Default)]
|
||||
pub struct MutationRoot(
|
||||
blog::BlogMutation,
|
||||
config::ConfigMutation,
|
||||
permissions::PermissionMutation,
|
||||
users::UserMutation,
|
||||
);
|
||||
148
src/graphql/mutations/blog.rs
Normal file
148
src/graphql/mutations/blog.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use crate::auth::get_auth_user;
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::*;
|
||||
use crate::services::blog_service::BlogService;
|
||||
use async_graphql::{Context, Error as GraphQLError, Object, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BlogMutation;
|
||||
|
||||
#[Object]
|
||||
impl BlogMutation {
|
||||
/// 创建博客文章
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
|
||||
async fn create_blog(&self, ctx: &Context<'_>, input: CreateBlogInput) -> Result<Blog> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.create_blog(input, auth_user.id)
|
||||
.await
|
||||
.map(|blog| blog.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 更新博客文章
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
|
||||
async fn update_blog(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
id: Uuid,
|
||||
input: UpdateBlogInput,
|
||||
) -> Result<Blog> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.update_blog(id, input, auth_user.id)
|
||||
.await
|
||||
.map(|blog| blog.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 删除博客文章
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blogs\")")]
|
||||
async fn delete_blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.delete_blog(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 增加博客浏览次数
|
||||
#[graphql(guard = "RequireReadPermission::new(\"blogs\")")]
|
||||
async fn increment_blog_view_count(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.increment_blog_view_count(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 创建博客分类
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
|
||||
async fn create_blog_category(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: CreateBlogCategoryInput,
|
||||
) -> Result<BlogCategory> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.create_category(input, auth_user.id)
|
||||
.await
|
||||
.map(|category| category.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 更新博客分类
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
|
||||
async fn update_blog_category(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
id: Uuid,
|
||||
input: UpdateBlogCategoryInput,
|
||||
) -> Result<BlogCategory> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.update_category(id, input, auth_user.id)
|
||||
.await
|
||||
.map(|category| category.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 删除博客分类
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_categories\")")]
|
||||
async fn delete_blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.delete_category(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 创建博客标签
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
|
||||
async fn create_blog_tag(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: CreateBlogTagInput,
|
||||
) -> Result<BlogTag> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.create_tag(input, auth_user.id)
|
||||
.await
|
||||
.map(|tag| tag.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 更新博客标签
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
|
||||
async fn update_blog_tag(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
id: Uuid,
|
||||
input: UpdateBlogTagInput,
|
||||
) -> Result<BlogTag> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.update_tag(id, input, auth_user.id)
|
||||
.await
|
||||
.map(|tag| tag.into())
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
|
||||
/// 删除博客标签
|
||||
#[graphql(guard = "RequireWritePermission::new(\"blog_tags\")")]
|
||||
async fn delete_blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<bool> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
blog_service
|
||||
.delete_tag(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))
|
||||
}
|
||||
}
|
||||
33
src/graphql/mutations/config.rs
Normal file
33
src/graphql/mutations/config.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::*;
|
||||
use crate::services::config_manager::ConfigsManager;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigMutation;
|
||||
|
||||
#[Object]
|
||||
impl ConfigMutation {
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
async fn update_config_batch(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
input: Vec<UpdateConfig>,
|
||||
) -> Result<String> {
|
||||
let config_manager = ctx.data::<ConfigsManager>()?;
|
||||
|
||||
let configs = input
|
||||
.into_iter()
|
||||
.map(|input| {
|
||||
(
|
||||
input.key,
|
||||
serde_json::to_value(input.value.unwrap()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<String, serde_json::Value>>();
|
||||
|
||||
config_manager.set_values(configs).await?;
|
||||
Ok("successed".to_string())
|
||||
}
|
||||
}
|
||||
86
src/graphql/mutations/permissions.rs
Normal file
86
src/graphql/mutations/permissions.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use crate::graphql::guards::*;
|
||||
use crate::services::casbin_service::CasbinService;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PermissionMutation;
|
||||
|
||||
#[Object]
|
||||
impl PermissionMutation {
|
||||
// 权限管理相关的 Mutation
|
||||
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
|
||||
async fn assign_role_to_user(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
user_id: Uuid,
|
||||
role_name: String,
|
||||
) -> Result<bool> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
casbin_service
|
||||
.assign_role(&user_id.to_string(), &role_name)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
|
||||
async fn remove_role_from_user(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
user_id: Uuid,
|
||||
role_name: String,
|
||||
) -> Result<bool> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
casbin_service
|
||||
.remove_role(&user_id.to_string(), &role_name)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
|
||||
async fn add_policy(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
role_name: String,
|
||||
resource: String,
|
||||
action: String,
|
||||
) -> Result<bool> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
casbin_service
|
||||
.add_policy(&role_name, &resource, &action)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
|
||||
async fn remove_policy(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
role_name: String,
|
||||
resource: String,
|
||||
action: String,
|
||||
) -> Result<bool> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
casbin_service
|
||||
.remove_policy(&role_name, &resource, &action)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireWritePermission::new(\"permissions\")")]
|
||||
async fn reload_policies(&self, ctx: &Context<'_>) -> Result<bool> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
casbin_service.reload_policy().await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
56
src/graphql/mutations/users.rs
Normal file
56
src/graphql/mutations/users.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::users::*;
|
||||
use crate::services::user_service::UserService;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserMutation;
|
||||
|
||||
#[Object]
|
||||
impl UserMutation {
|
||||
async fn register(&self, ctx: &Context<'_>, input: RegisterInput) -> Result<User> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
user_service.register(input).await.map(|user| user.into())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireWritePermission::new(\"users\")")]
|
||||
async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> Result<User> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
user_service
|
||||
.create_user(input)
|
||||
.await
|
||||
.map(|user| user.into())
|
||||
}
|
||||
|
||||
async fn login(&self, ctx: &Context<'_>, input: LoginInput) -> Result<LoginResponse> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
user_service.login(input).await.map(|user| user.into())
|
||||
}
|
||||
|
||||
// async fn create_invite_code(
|
||||
// &self,
|
||||
// ctx: &Context<'_>,
|
||||
// input: CreateInviteCodeInput,
|
||||
// ) -> Result<InviteCodeResponse> {
|
||||
// let auth_user = get_auth_user(ctx).await?;
|
||||
// let invite_code_service = ctx.data::<InviteCodeService>()?;
|
||||
|
||||
// let code = invite_code_service
|
||||
// .create_invite_code(auth_user.id, input)
|
||||
// .await?;
|
||||
|
||||
// // Get the invite code details to return expires_at
|
||||
// let invite_codes = invite_code_service
|
||||
// .get_invite_codes_by_creator(auth_user.id)
|
||||
// .await?;
|
||||
// let invite_code = invite_codes
|
||||
// .into_iter()
|
||||
// .find(|ic| ic.code == code)
|
||||
// .ok_or_else(|| async_graphql::Error::new("Failed to retrieve created invite code"))?;
|
||||
|
||||
// Ok(InviteCodeResponse {
|
||||
// code,
|
||||
// expires_at: invite_code.expires_at,
|
||||
// })
|
||||
// }
|
||||
}
|
||||
13
src/graphql/queries.rs
Normal file
13
src/graphql/queries.rs
Normal file
@ -0,0 +1,13 @@
|
||||
mod blog;
|
||||
mod config;
|
||||
mod permissions;
|
||||
mod user;
|
||||
use async_graphql::MergedObject;
|
||||
|
||||
#[derive(MergedObject, Default)]
|
||||
pub struct QueryRoot(
|
||||
blog::BlogQuery,
|
||||
config::ConfigQuery,
|
||||
permissions::PermissionQuery,
|
||||
user::UserQuery,
|
||||
);
|
||||
120
src/graphql/queries/blog.rs
Normal file
120
src/graphql/queries/blog.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use crate::graphql::types::{blog::*, PaginatedResult, PaginationInput};
|
||||
use crate::services::blog_service::BlogService;
|
||||
use async_graphql::{Context, Error as GraphQLError, Object, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BlogQuery;
|
||||
|
||||
#[Object]
|
||||
impl BlogQuery {
|
||||
async fn blogs(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
filter: Option<BlogFilterInput>,
|
||||
sort: Option<BlogSortInput>,
|
||||
pagination: Option<PaginationInput>,
|
||||
) -> Result<PaginatedResult<Blog>> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let result = blog_service
|
||||
.get_blogs(filter, sort, pagination)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
|
||||
// 转换 model 类型到 GraphQL 类型
|
||||
Ok(PaginatedResult::new(
|
||||
result.items.into_iter().map(|item| item.into()).collect(),
|
||||
result.total,
|
||||
result.page,
|
||||
result.per_page,
|
||||
))
|
||||
}
|
||||
|
||||
/// 根据ID获取博客文章
|
||||
async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result<Blog> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let blog = blog_service
|
||||
.get_blog_by_id(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(blog.into())
|
||||
}
|
||||
|
||||
/// 根据slug获取博客文章
|
||||
async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result<Blog> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let blog = blog_service
|
||||
.get_blog_by_slug(&slug)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(blog.into())
|
||||
}
|
||||
|
||||
async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogDetail> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let detail = blog_service
|
||||
.get_blog_detail(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
|
||||
// 手动转换 BlogDetail,因为它包含嵌套结构
|
||||
Ok(BlogDetail {
|
||||
blog: detail.blog.into(),
|
||||
category: detail.category.map(|c| c.into()),
|
||||
tags: detail.tags.into_iter().map(|t| t.into()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn blog_stats(&self, ctx: &Context<'_>) -> Result<BlogStats> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let stats = blog_service
|
||||
.get_blog_stats()
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(stats.into())
|
||||
}
|
||||
|
||||
async fn blog_categories(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
filter: Option<BlogCategoryFilterInput>,
|
||||
) -> Result<Vec<BlogCategory>> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let categories = blog_service
|
||||
.get_categories(filter)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(categories.into_iter().map(|c| c.into()).collect())
|
||||
}
|
||||
|
||||
async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogCategory> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let category = blog_service
|
||||
.get_category_by_id(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(category.into())
|
||||
}
|
||||
|
||||
async fn blog_tags(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
filter: Option<BlogTagFilterInput>,
|
||||
) -> Result<Vec<BlogTag>> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let tags = blog_service
|
||||
.get_tags(filter)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(tags.into_iter().map(|t| t.into()).collect())
|
||||
}
|
||||
|
||||
async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogTag> {
|
||||
let blog_service = ctx.data::<BlogService>()?;
|
||||
let tag = blog_service
|
||||
.get_tag_by_id(id)
|
||||
.await
|
||||
.map_err(|e| GraphQLError::new(e.to_string()))?;
|
||||
Ok(tag.into())
|
||||
}
|
||||
}
|
||||
23
src/graphql/queries/config.rs
Normal file
23
src/graphql/queries/config.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::{config::*, permission::*};
|
||||
use crate::services::config_manager::ConfigsManager;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ConfigQuery;
|
||||
|
||||
#[Object]
|
||||
impl ConfigQuery {
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
async fn configs(&self, ctx: &Context<'_>) -> Result<Vec<Config>> {
|
||||
let configs_service = ctx.data::<ConfigsManager>()?;
|
||||
let configs = configs_service.get_all_settings().await?;
|
||||
Ok(configs)
|
||||
}
|
||||
|
||||
async fn site_configs(&self, ctx: &Context<'_>) -> Result<Vec<Config>> {
|
||||
let configs_service = ctx.data::<ConfigsManager>()?;
|
||||
let configs = configs_service.get_settings_by_category("site").await?;
|
||||
Ok(configs)
|
||||
}
|
||||
}
|
||||
132
src/graphql/queries/permissions.rs
Normal file
132
src/graphql/queries/permissions.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use crate::auth::get_auth_user;
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::*;
|
||||
use crate::services::casbin_service::CasbinService;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PermissionQuery;
|
||||
|
||||
#[Object]
|
||||
impl PermissionQuery {
|
||||
// 权限管理查询
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn check_permission(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
resource: String,
|
||||
action: String,
|
||||
) -> Result<PermissionCheckResult> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let has_permission = casbin_service
|
||||
.check_permission(&user.id.to_string(), &resource, &action)
|
||||
.await?;
|
||||
|
||||
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
|
||||
|
||||
Ok(PermissionCheckResult {
|
||||
user_id: user.id.to_string(),
|
||||
resource,
|
||||
action,
|
||||
has_permission,
|
||||
roles,
|
||||
})
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn get_user_roles(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let roles = casbin_service.get_user_roles(&user.id.to_string()).await?;
|
||||
Ok(roles)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireLogin")]
|
||||
async fn get_user_permissions(&self, ctx: &Context<'_>) -> Result<Vec<PermissionPair>> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let permissions = casbin_service
|
||||
.get_user_permissions(&user.id.to_string())
|
||||
.await?;
|
||||
|
||||
Ok(permissions
|
||||
.into_iter()
|
||||
.map(|p| PermissionPair {
|
||||
resource: p.0,
|
||||
action: p.1,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn get_all_policies(&self, ctx: &Context<'_>) -> Result<Vec<PolicyType>> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let policies = casbin_service.get_all_policies().await?;
|
||||
|
||||
Ok(policies
|
||||
.into_iter()
|
||||
.filter(|p| p.len() >= 3)
|
||||
.map(|p| PolicyType {
|
||||
role: p[0].clone(),
|
||||
resource: p[1].clone(),
|
||||
action: p[2].clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn get_role_permissions(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
role_name: String,
|
||||
) -> Result<Vec<PermissionPair>> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let permissions = casbin_service.get_role_permissions(&role_name).await?;
|
||||
Ok(permissions
|
||||
.into_iter()
|
||||
.map(|p| PermissionPair {
|
||||
resource: p.0,
|
||||
action: p.1,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let can_read = casbin_service
|
||||
.can_read(&user.id.to_string(), &resource)
|
||||
.await?;
|
||||
Ok(can_read)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let can_write = casbin_service
|
||||
.can_write(&user.id.to_string(), &resource)
|
||||
.await?;
|
||||
Ok(can_write)
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"permissions\")")]
|
||||
async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result<bool> {
|
||||
let user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let can_delete = casbin_service
|
||||
.can_delete(&user.id.to_string(), &resource)
|
||||
.await?;
|
||||
Ok(can_delete)
|
||||
}
|
||||
}
|
||||
166
src/graphql/queries/user.rs
Normal file
166
src/graphql/queries/user.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use crate::auth::get_auth_user;
|
||||
use crate::graphql::guards::*;
|
||||
use crate::graphql::types::users::*;
|
||||
use crate::services::casbin_service::CasbinService;
|
||||
use crate::services::user_service::UserService;
|
||||
use async_graphql::{Context, Object, Result};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::filter;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UserQuery;
|
||||
|
||||
#[Object]
|
||||
impl UserQuery {
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn current_user(&self, ctx: &Context<'_>) -> Result<User> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
|
||||
user_service
|
||||
.get_user_by_id(auth_user.id)
|
||||
.await?
|
||||
.map(|user| user.into())
|
||||
.ok_or_else(|| async_graphql::Error::new("User not found"))
|
||||
}
|
||||
|
||||
/// 获取当前用户的组信息
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn current_user_groups(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let groups = casbin_service
|
||||
.get_user_roles(&auth_user.id.to_string())
|
||||
.await
|
||||
.map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?;
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// 获取当前用户信息(包含组)
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn current_user_with_groups(&self, ctx: &Context<'_>) -> Result<UserWithGroups> {
|
||||
let auth_user = get_auth_user(ctx).await?;
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
|
||||
let user_with_groups = user_service
|
||||
.get_user_with_groups(auth_user.id)
|
||||
.await?
|
||||
.ok_or_else(|| async_graphql::Error::new("User not found"))?;
|
||||
|
||||
Ok(user_with_groups.into())
|
||||
}
|
||||
|
||||
/// 获取指定用户的组信息
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn user_groups(&self, ctx: &Context<'_>, user_id: uuid::Uuid) -> Result<Vec<String>> {
|
||||
let casbin_service = ctx.data::<CasbinService>()?;
|
||||
|
||||
let groups = casbin_service
|
||||
.get_user_roles(&user_id.to_string())
|
||||
.await
|
||||
.map_err(|e| async_graphql::Error::new(format!("Failed to get user groups: {}", e)))?;
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
/// 获取指定用户信息(包含组)
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn user_with_groups(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
user_id: uuid::Uuid,
|
||||
) -> Result<UserWithGroups> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
|
||||
let user_with_groups = user_service
|
||||
.get_user_with_groups(user_id)
|
||||
.await?
|
||||
.ok_or_else(|| async_graphql::Error::new("User not found"))?;
|
||||
|
||||
Ok(user_with_groups.into())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn users(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
offset: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
sort_by: Option<String>,
|
||||
sort_order: Option<String>,
|
||||
filter: Option<String>,
|
||||
) -> Result<Vec<User>> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
info!("users im here");
|
||||
let offset = offset.unwrap_or(0);
|
||||
let limit = limit.unwrap_or(20);
|
||||
let sort_by = sort_by.unwrap_or("created_at".to_string());
|
||||
let sort_order = sort_order.unwrap_or("desc".to_string());
|
||||
user_service
|
||||
.get_all_users(offset, limit, sort_by, sort_order, filter)
|
||||
.await
|
||||
.map(|user| user.into_iter().map(|u| u.into()).collect())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn users_with_groups(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
offset: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
sort_by: Option<String>,
|
||||
sort_order: Option<String>,
|
||||
filter: Option<String>,
|
||||
) -> Result<Vec<UserWithGroups>> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
let offset = offset.unwrap_or(0);
|
||||
let limit = limit.unwrap_or(20);
|
||||
let sort_by = sort_by.unwrap_or("created_at".to_string());
|
||||
let sort_order = sort_order.unwrap_or("desc".to_string());
|
||||
user_service
|
||||
.get_all_users_with_groups(offset, limit, sort_by, sort_order, filter)
|
||||
.await
|
||||
.map(|user| user.into_iter().map(|u| u.into()).collect())
|
||||
}
|
||||
|
||||
#[graphql(guard = "RequireReadPermission::new(\"users\")")]
|
||||
async fn users_info(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
offset: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
sort_by: Option<String>,
|
||||
sort_order: Option<String>,
|
||||
filter: Option<String>,
|
||||
) -> Result<UserInfoRespnose> {
|
||||
let user_service = ctx.data::<UserService>()?;
|
||||
let offset = offset.unwrap_or(0);
|
||||
let limit = limit.unwrap_or(20);
|
||||
let sort_by = sort_by.unwrap_or("created_at".to_string());
|
||||
let sort_order = sort_order.unwrap_or("desc".to_string());
|
||||
|
||||
user_service
|
||||
.users_info(offset, limit, sort_by, sort_order, filter)
|
||||
.await
|
||||
.map(|r| r.into())
|
||||
}
|
||||
|
||||
// #[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")]
|
||||
// async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result<Vec<InviteCode>> {
|
||||
// let auth_user = get_auth_user(ctx).await?;
|
||||
// let invite_code_service = ctx.data::<InviteCodeService>()?;
|
||||
// invite_code_service
|
||||
// .get_invite_codes_by_creator(auth_user.id)
|
||||
// .await
|
||||
// }
|
||||
|
||||
// #[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")]
|
||||
// async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result<bool> {
|
||||
// let invite_code_service = ctx.data::<InviteCodeService>()?;
|
||||
// invite_code_service
|
||||
// .validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code })
|
||||
// .await
|
||||
// }
|
||||
}
|
||||
1336
src/graphql/query.rs
1336
src/graphql/query.rs
File diff suppressed because it is too large
Load Diff
@ -1,701 +0,0 @@
|
||||
use crate::models::{user::Role, Setting};
|
||||
use async_graphql::{InputObject, SimpleObject, Union};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct RegisterInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub invite_code: String,
|
||||
pub role: Option<Role>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateUserInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub role: Option<Role>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct LoginInput {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateInviteCodeInput {
|
||||
pub expires_in_days: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct ValidateInviteCodeInput {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct InviteCodeResponse {
|
||||
pub code: String,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct InitializeAdminInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct InitializeAdminResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub user: Option<crate::models::user::User>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct UserInfoRespnose {
|
||||
pub total_users: i64,
|
||||
pub total_active_users: i64,
|
||||
pub total_inactive_users: i64,
|
||||
pub total_admin_users: i64,
|
||||
pub total_user_users: i64,
|
||||
|
||||
pub users: Vec<crate::models::user::UserInfoRow>,
|
||||
}
|
||||
|
||||
// Settings GraphQL types
|
||||
#[derive(SimpleObject, Debug, Clone)]
|
||||
pub struct SettingType {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<Setting> for SettingType {
|
||||
fn from(setting: Setting) -> Self {
|
||||
SettingType {
|
||||
id: setting.id,
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
value_type: setting.value_type,
|
||||
description: setting.description,
|
||||
category: setting.category,
|
||||
is_encrypted: setting.is_encrypted,
|
||||
is_system: setting.is_system,
|
||||
is_editable: setting.is_editable,
|
||||
created_at: setting.created_at,
|
||||
updated_at: setting.updated_at,
|
||||
created_by: setting.created_by,
|
||||
updated_by: setting.updated_by,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateSettingInput {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: String,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct UpdateSettingInput {
|
||||
pub value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub is_editable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct SettingFilterInput {
|
||||
pub category: Option<String>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingHistoryType {
|
||||
pub id: Uuid,
|
||||
pub setting_id: Uuid,
|
||||
pub old_value: Option<String>,
|
||||
pub new_value: Option<String>,
|
||||
pub changed_by: Option<Uuid>,
|
||||
pub change_reason: Option<String>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingsStatsType {
|
||||
pub categories: Vec<String>,
|
||||
pub stats: std::collections::HashMap<String, i64>,
|
||||
}
|
||||
|
||||
// Page Block GraphQL types
|
||||
#[derive(SimpleObject)]
|
||||
pub struct PageType {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Union)]
|
||||
pub enum BlockType {
|
||||
TextBlock(TextBlockType),
|
||||
ChartBlock(ChartBlockType),
|
||||
SettingsBlock(SettingsBlockType),
|
||||
TableBlock(TableBlockType),
|
||||
HeroBlock(HeroBlockType),
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TextBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct ChartBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String,
|
||||
pub series: Vec<DataPointType>,
|
||||
pub config: Option<Value>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct DataPointType {
|
||||
pub id: Uuid,
|
||||
pub chart_block_id: Uuid,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingsBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TableBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub columns: Vec<TableColumnType>,
|
||||
pub data_source: String,
|
||||
pub data_config: Option<Value>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct TableColumnType {
|
||||
pub id: Uuid,
|
||||
pub table_block_id: Uuid,
|
||||
pub name: String,
|
||||
pub label: String,
|
||||
pub data_type: String,
|
||||
pub is_sortable: bool,
|
||||
pub is_filterable: bool,
|
||||
pub width: Option<i32>,
|
||||
pub order: i32,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct HeroBlockType {
|
||||
pub id: Uuid,
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub subtitle: Option<String>,
|
||||
pub background_image: Option<String>,
|
||||
pub background_color: Option<String>,
|
||||
pub text_color: Option<String>,
|
||||
pub cta_text: Option<String>,
|
||||
pub cta_link: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
// Page Block Input types
|
||||
#[derive(InputObject)]
|
||||
pub struct CreatePageInputType {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct UpdatePageInputType {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateTextBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub markdown: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateChartBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: String,
|
||||
pub chart_type: String,
|
||||
pub series: Vec<CreateDataPointInputType>,
|
||||
pub config: Option<Value>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateDataPointInputType {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub label: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateSettingsBlockInputType {
|
||||
pub page_id: Uuid,
|
||||
pub block_order: i32,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub editable: bool,
|
||||
pub display_mode: String,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct PageFilterInputType {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// Enhanced Settings types for the settings center
|
||||
#[derive(SimpleObject)]
|
||||
pub struct SettingCenterType {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub description: Option<String>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct UpdateSettingCenterInput {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct BatchUpdateSettingsInput {
|
||||
pub updates: Vec<UpdateSettingCenterInput>,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// 配置分类页面类型,包含页面信息和相关配置项
|
||||
#[derive(SimpleObject)]
|
||||
pub struct CategoryPageType {
|
||||
/// 页面信息,如果不存在则为 None
|
||||
pub page: Option<PageType>,
|
||||
/// 该分类下的所有配置项
|
||||
pub settings: Vec<SettingCenterType>,
|
||||
/// 分类名称
|
||||
pub category: String,
|
||||
/// 该分类下的配置项总数
|
||||
pub settings_count: i32,
|
||||
/// 该分类下的系统配置项数量
|
||||
pub system_settings_count: i32,
|
||||
/// 该分类下的可编辑配置项数量
|
||||
pub editable_settings_count: i32,
|
||||
}
|
||||
|
||||
/// 权限类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionType {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 角色权限关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct RolePermissionType {
|
||||
pub id: Uuid,
|
||||
pub role_name: String,
|
||||
pub permission: PermissionType,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 用户角色关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct UserRoleType {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// 权限策略类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PolicyType {
|
||||
pub role: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// 权限检查结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionCheckResult {
|
||||
pub user_id: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub has_permission: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
/// 权限管理查询输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct PermissionFilterInput {
|
||||
pub resource: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub role_name: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
/// 角色权限分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignRolePermissionInput {
|
||||
pub role_name: String,
|
||||
pub permission_id: Uuid,
|
||||
pub granted_by: Uuid,
|
||||
}
|
||||
|
||||
/// 用户角色分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignUserRoleInput {
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Uuid,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionPair {
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
// 站点与运营配置相关类型
|
||||
|
||||
/// 站点基本信息类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteInfoType {
|
||||
pub name: String,
|
||||
pub locale_default: String,
|
||||
pub locales_supported: Vec<String>,
|
||||
}
|
||||
|
||||
/// 品牌配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct BrandConfigType {
|
||||
pub logo_url: String,
|
||||
pub primary_color: String,
|
||||
pub dark_mode_default: bool,
|
||||
}
|
||||
|
||||
/// 页脚链接类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct FooterLinkType {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub visible_to_guest: bool,
|
||||
}
|
||||
|
||||
/// 站点配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteConfigType {
|
||||
pub info: SiteInfoType,
|
||||
pub brand: BrandConfigType,
|
||||
pub footer_links: Vec<FooterLinkType>,
|
||||
}
|
||||
|
||||
/// 横幅公告类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct BannerNoticeType {
|
||||
pub enabled: bool,
|
||||
pub text: std::collections::HashMap<String, String>, // 多语言文本
|
||||
}
|
||||
|
||||
/// 维护窗口类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct MaintenanceWindowType {
|
||||
pub enabled: bool,
|
||||
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub message: std::collections::HashMap<String, String>, // 多语言消息
|
||||
}
|
||||
|
||||
/// 弹窗公告类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ModalAnnouncementType {
|
||||
pub id: String,
|
||||
pub title: std::collections::HashMap<String, String>, // 多语言标题
|
||||
pub content: std::collections::HashMap<String, String>, // 多语言内容
|
||||
pub start_time: chrono::DateTime<chrono::Utc>,
|
||||
pub end_time: chrono::DateTime<chrono::Utc>,
|
||||
pub audience: Vec<String>,
|
||||
pub priority: String,
|
||||
}
|
||||
|
||||
/// 公告维护配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct NoticeMaintenanceType {
|
||||
pub banner: BannerNoticeType,
|
||||
pub maintenance_window: MaintenanceWindowType,
|
||||
pub modal_announcements: Vec<ModalAnnouncementType>,
|
||||
}
|
||||
|
||||
/// 文档链接类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct DocLinkType {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 聊天群组类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ChatGroupType {
|
||||
pub name: String,
|
||||
pub url: Option<String>,
|
||||
pub qr_code: Option<String>,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 支持渠道类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SupportChannelsType {
|
||||
pub email: String,
|
||||
pub ticket_system: String,
|
||||
pub chat_groups: Vec<ChatGroupType>,
|
||||
pub working_hours: std::collections::HashMap<String, String>, // 多语言工作时间
|
||||
}
|
||||
|
||||
/// 文档支持配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct DocsSupportType {
|
||||
pub links: Vec<DocLinkType>,
|
||||
pub channels: SupportChannelsType,
|
||||
}
|
||||
|
||||
/// 功能开关类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct FeatureSwitchesType {
|
||||
pub registration_enabled: bool,
|
||||
pub invite_code_required: bool,
|
||||
pub email_verification: bool,
|
||||
}
|
||||
|
||||
/// 限制配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct LimitsConfigType {
|
||||
pub max_users: i32,
|
||||
pub max_invite_codes_per_user: i32,
|
||||
pub session_timeout_hours: i32,
|
||||
}
|
||||
|
||||
/// 通知配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct NotificationConfigType {
|
||||
pub welcome_email: bool,
|
||||
pub system_announcements: bool,
|
||||
pub maintenance_alerts: bool,
|
||||
}
|
||||
|
||||
/// 运营配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct OpsConfigType {
|
||||
pub features: FeatureSwitchesType,
|
||||
pub limits: LimitsConfigType,
|
||||
pub notifications: NotificationConfigType,
|
||||
}
|
||||
|
||||
/// 完整的站点与运营配置类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct SiteOpsConfigType {
|
||||
pub site: SiteConfigType,
|
||||
pub notice_maintenance: NoticeMaintenanceType,
|
||||
pub docs_support: DocsSupportType,
|
||||
pub ops: OpsConfigType,
|
||||
}
|
||||
|
||||
/// 更新站点配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateSiteConfigInput {
|
||||
pub name: Option<String>,
|
||||
pub locale_default: Option<String>,
|
||||
pub locales_supported: Option<Vec<String>>,
|
||||
pub logo_url: Option<String>,
|
||||
pub primary_color: Option<String>,
|
||||
pub dark_mode_default: Option<bool>,
|
||||
pub footer_links: Option<Vec<FooterLinkInput>>,
|
||||
}
|
||||
|
||||
/// 页脚链接输入类型
|
||||
#[derive(Debug, InputObject, Serialize, Deserialize)]
|
||||
pub struct FooterLinkInput {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub visible_to_guest: bool,
|
||||
}
|
||||
|
||||
/// 更新公告配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateNoticeConfigInput {
|
||||
pub banner_enabled: Option<bool>,
|
||||
pub banner_text: Option<std::collections::HashMap<String, String>>,
|
||||
pub maintenance_enabled: Option<bool>,
|
||||
pub maintenance_start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub maintenance_end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub maintenance_message: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 更新弹窗公告输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateModalAnnouncementInput {
|
||||
pub id: String,
|
||||
pub title: Option<std::collections::HashMap<String, String>>,
|
||||
pub content: Option<std::collections::HashMap<String, String>>,
|
||||
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub audience: Option<Vec<String>>,
|
||||
pub priority: Option<String>,
|
||||
}
|
||||
|
||||
/// 更新文档支持配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateDocsSupportInput {
|
||||
pub links: Option<Vec<DocLinkInput>>,
|
||||
pub email: Option<String>,
|
||||
pub ticket_system: Option<String>,
|
||||
pub working_hours: Option<std::collections::HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// 文档链接输入类型
|
||||
#[derive(Debug, InputObject, Serialize, Deserialize)]
|
||||
pub struct DocLinkInput {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// 更新运营配置输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct UpdateOpsConfigInput {
|
||||
pub registration_enabled: Option<bool>,
|
||||
pub invite_code_required: Option<bool>,
|
||||
pub email_verification: Option<bool>,
|
||||
pub max_users: Option<i32>,
|
||||
pub max_invite_codes_per_user: Option<i32>,
|
||||
pub session_timeout_hours: Option<i32>,
|
||||
pub welcome_email: Option<bool>,
|
||||
pub system_announcements: Option<bool>,
|
||||
pub maintenance_alerts: Option<bool>,
|
||||
}
|
||||
|
||||
/// 配置更新结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ConfigUpdateResultType {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub updated_settings: Vec<SettingType>,
|
||||
}
|
||||
|
||||
/// 配置验证结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct ConfigValidationResultType {
|
||||
pub valid: bool,
|
||||
pub errors: Vec<String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
316
src/graphql/types/blog.rs
Normal file
316
src/graphql/types/blog.rs
Normal file
@ -0,0 +1,316 @@
|
||||
pub use async_graphql::{InputObject, SimpleObject};
|
||||
pub use chrono::{DateTime, Utc};
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::models::blog::{
|
||||
Blog as ModelBlog, BlogCategory as ModelBlogCategory, BlogTag as ModelBlogTag,
|
||||
};
|
||||
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
|
||||
pub mod output {
|
||||
use super::*;
|
||||
use crate::from_model;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct BlogCategory {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub sort_order: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// 博客标签 GraphQL 类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct BlogTag {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// 博客文章 GraphQL 类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct Blog {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub excerpt: Option<String>,
|
||||
pub content: serde_json::Value,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: String,
|
||||
pub featured_image: Option<String>,
|
||||
pub meta_title: Option<String>,
|
||||
pub meta_description: Option<String>,
|
||||
pub published_at: Option<DateTime<Utc>>,
|
||||
pub view_count: i32,
|
||||
pub is_featured: bool,
|
||||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
// 使用宏自动实现 From trait
|
||||
from_model!(
|
||||
BlogCategory,
|
||||
ModelBlogCategory,
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
color,
|
||||
icon,
|
||||
is_active,
|
||||
sort_order,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
);
|
||||
|
||||
from_model!(
|
||||
BlogTag,
|
||||
ModelBlogTag,
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
color,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
);
|
||||
|
||||
from_model!(
|
||||
Blog,
|
||||
ModelBlog,
|
||||
id,
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
category_id,
|
||||
status,
|
||||
featured_image,
|
||||
meta_title,
|
||||
meta_description,
|
||||
published_at,
|
||||
view_count,
|
||||
is_featured,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by,
|
||||
updated_by
|
||||
);
|
||||
|
||||
/// 博客文章详情(包含分类和标签信息)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct BlogDetail {
|
||||
#[graphql(flatten)]
|
||||
pub blog: Blog,
|
||||
pub category: Option<BlogCategory>,
|
||||
pub tags: Vec<BlogTag>,
|
||||
}
|
||||
|
||||
/// 博客标签关联 GraphQL 类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct BlogTagRelation {
|
||||
pub id: Uuid,
|
||||
pub blog_id: Uuid,
|
||||
pub tag_id: Uuid,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// 为 BlogTagRelation 实现 From trait
|
||||
from_model!(
|
||||
BlogTagRelation,
|
||||
crate::models::blog::BlogTagRelation,
|
||||
id,
|
||||
blog_id,
|
||||
tag_id,
|
||||
created_at
|
||||
);
|
||||
|
||||
// 为 BlogStats 实现 From trait
|
||||
from_model!(
|
||||
BlogStats,
|
||||
crate::models::blog::BlogStats,
|
||||
total_blogs,
|
||||
published_blogs,
|
||||
draft_blogs,
|
||||
archived_blogs,
|
||||
total_categories,
|
||||
total_tags,
|
||||
total_views
|
||||
);
|
||||
|
||||
// 博客统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct BlogStats {
|
||||
pub total_blogs: i64,
|
||||
pub published_blogs: i64,
|
||||
pub draft_blogs: i64,
|
||||
pub archived_blogs: i64,
|
||||
pub total_categories: i64,
|
||||
pub total_tags: i64,
|
||||
pub total_views: i64,
|
||||
}
|
||||
|
||||
// 注意:业务逻辑方法已经在 models/blog.rs 中实现
|
||||
// 这里的 GraphQL 类型主要用于 API 输出,业务逻辑应该在 model 层处理
|
||||
|
||||
impl BlogDetail {
|
||||
/// 从博客文章创建详情对象
|
||||
pub fn new(blog: Blog, category: Option<BlogCategory>, tags: Vec<BlogTag>) -> Self {
|
||||
Self {
|
||||
blog,
|
||||
category,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod input {
|
||||
use super::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
// 创建博客分类输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogCategoryInput {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
// 更新博客分类输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogCategoryInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
// 创建博客标签输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogTagInput {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新博客标签输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogTagInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建博客文章输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogInput {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub excerpt: Option<String>,
|
||||
pub content: serde_json::Value,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub featured_image: Option<String>,
|
||||
pub meta_title: Option<String>,
|
||||
pub meta_description: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
// 更新博客文章输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub excerpt: Option<String>,
|
||||
pub content: Option<serde_json::Value>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub featured_image: Option<String>,
|
||||
pub meta_title: Option<String>,
|
||||
pub meta_description: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
// 博客过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogFilterInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub search: Option<String>,
|
||||
pub date_from: Option<DateTime<Utc>>,
|
||||
pub date_to: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// 博客分类过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogCategoryFilterInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// 博客标签过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogTagFilterInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// 博客排序
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogSortInput {
|
||||
pub field: String,
|
||||
pub direction: String, // "asc" or "desc"
|
||||
}
|
||||
}
|
||||
169
src/graphql/types/config.rs
Normal file
169
src/graphql/types/config.rs
Normal file
@ -0,0 +1,169 @@
|
||||
use crate::graphql::guards::*;
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct ConfigValue {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
pub mod output {
|
||||
use serde::de::Error;
|
||||
|
||||
use super::*;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
pub struct Config {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub category: Option<String>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub is_encrypted: Option<bool>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub is_system: Option<bool>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub is_editable: Option<bool>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub created_by: Option<Uuid>,
|
||||
#[graphql(guard = "RequireWritePermission::new(\"config\")")]
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// 获取配置值的类型化版本
|
||||
pub fn get_typed_value<T>(&self) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
match &self.value {
|
||||
Some(v) => serde_json::from_str(v),
|
||||
None => Err(serde_json::Error::custom("No value set")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置配置值
|
||||
pub fn set_value<T>(&mut self, value: &T) -> Result<(), serde_json::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
self.value = Some(serde_json::to_string(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查是否为特定类型
|
||||
pub fn is_type(&self, expected_type: &str) -> bool {
|
||||
self.value_type == expected_type
|
||||
}
|
||||
|
||||
/// 获取布尔值
|
||||
pub fn get_bool(&self) -> Result<bool, String> {
|
||||
if self.value_type == "boolean" {
|
||||
self.value
|
||||
.as_ref()
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.ok_or_else(|| "Invalid boolean value".to_string())
|
||||
} else {
|
||||
Err("Setting is not a boolean type".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数字值
|
||||
pub fn get_number(&self) -> Result<f64, String> {
|
||||
if self.value_type == "number" {
|
||||
self.value
|
||||
.as_ref()
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.ok_or_else(|| "Invalid number value".to_string())
|
||||
} else {
|
||||
Err("Setting is not a number type".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取JSON值
|
||||
pub fn get_json(&self) -> Result<serde_json::Value, serde_json::Error> {
|
||||
if self.value_type == "json" {
|
||||
match &self.value {
|
||||
Some(v) => serde_json::from_str(v),
|
||||
None => Ok(serde_json::Value::Null),
|
||||
}
|
||||
} else {
|
||||
Err(serde_json::Error::custom("Setting is not a JSON type"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取字符串值
|
||||
pub fn get_string(&self) -> Result<String, String> {
|
||||
if self.value_type == "string" {
|
||||
Ok(self.value.clone().unwrap_or_default())
|
||||
} else {
|
||||
Err("Setting is not a string type".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::nil(),
|
||||
key: String::new(),
|
||||
value: None,
|
||||
value_type: "string".to_string(),
|
||||
description: None,
|
||||
category: Some("general".to_string()),
|
||||
is_encrypted: Some(false),
|
||||
is_system: Some(false),
|
||||
is_editable: Some(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod input {
|
||||
use super::*;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateConfig {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub is_editable: Option<bool>,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigFilter {
|
||||
pub category: Option<String>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigHistory {
|
||||
pub id: Uuid,
|
||||
pub setting_id: Uuid,
|
||||
pub old_value: Option<String>,
|
||||
pub new_value: Option<String>,
|
||||
pub changed_by: Option<Uuid>,
|
||||
pub change_reason: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
}
|
||||
123
src/graphql/types/mod.rs
Normal file
123
src/graphql/types/mod.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod blog;
|
||||
pub mod config;
|
||||
pub mod permission;
|
||||
pub mod users;
|
||||
|
||||
/// 用于自动实现 From<Model> for GraphQLType 的宏
|
||||
///
|
||||
/// 这个宏假设 GraphQL 类型和 Model 类型具有相同的字段名和类型。
|
||||
///
|
||||
/// # 用法
|
||||
/// ```rust
|
||||
/// from_model!(GraphQLType, ModelType, field1, field2, field3, ...);
|
||||
/// ```
|
||||
///
|
||||
/// # 示例
|
||||
/// ```rust
|
||||
/// from_model!(BlogCategory, crate::models::blog::BlogCategory,
|
||||
/// id, name, slug, description, color, icon, is_active, sort_order,
|
||||
/// created_at, updated_at, created_by, updated_by
|
||||
/// );
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! from_model {
|
||||
($graphql_type:ident, $model_type:ty, $( $field:ident ),* $(,)?) => {
|
||||
impl From<$model_type> for $graphql_type {
|
||||
fn from(model: $model_type) -> Self {
|
||||
Self {
|
||||
$(
|
||||
$field: model.$field,
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 更灵活的宏,支持字段映射和转换
|
||||
///
|
||||
/// # 用法
|
||||
/// ```rust
|
||||
/// from_model_with_mapping!(
|
||||
/// GraphQLType,
|
||||
/// ModelType,
|
||||
/// field1,
|
||||
/// field2 => |val| val.map(|v| v.to_string()),
|
||||
/// field3 => |val| val.into()
|
||||
/// );
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! from_model_with_mapping {
|
||||
(
|
||||
$graphql_type:ident,
|
||||
$model_type:ty,
|
||||
$( $field:ident $( => $mapping:expr )? ),* $(,)?
|
||||
) => {
|
||||
impl From<$model_type> for $graphql_type {
|
||||
fn from(model: $model_type) -> Self {
|
||||
Self {
|
||||
$(
|
||||
$field: from_model_with_mapping!(@apply_mapping model.$field, $( $mapping )?),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 没有映射,直接使用字段值
|
||||
(@apply_mapping $value:expr,) => {
|
||||
$value
|
||||
};
|
||||
|
||||
// 有映射函数,应用映射
|
||||
(@apply_mapping $value:expr, $mapping:expr) => {
|
||||
($mapping)($value)
|
||||
};
|
||||
}
|
||||
|
||||
pub use blog::*;
|
||||
pub use config::*;
|
||||
pub use config::*;
|
||||
pub use permission::*;
|
||||
pub use users::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct PaginationInput {
|
||||
pub page: Option<i32>,
|
||||
pub per_page: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PaginatedResult<T: async_graphql::OutputType + Send + Sync> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub per_page: i32,
|
||||
pub total_pages: i32,
|
||||
}
|
||||
|
||||
impl<T: async_graphql::OutputType + Send + Sync> PaginatedResult<T> {
|
||||
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
|
||||
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
|
||||
Self {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PageStats {
|
||||
pub text_blocks: i32,
|
||||
pub chart_blocks: i32,
|
||||
pub settings_blocks: i32,
|
||||
pub table_blocks: i32,
|
||||
pub hero_blocks: i32,
|
||||
pub total_blocks: i32,
|
||||
}
|
||||
102
src/graphql/types/permission.rs
Normal file
102
src/graphql/types/permission.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 权限类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionType {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub is_active: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 角色权限关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct RolePermissionType {
|
||||
pub id: Uuid,
|
||||
pub role_name: String,
|
||||
pub permission: PermissionType,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 用户角色关联类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct UserRoleType {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Option<Uuid>,
|
||||
pub granted_at: chrono::DateTime<chrono::Utc>,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
/// 权限策略类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PolicyType {
|
||||
pub role: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
/// 权限检查结果类型
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionCheckResult {
|
||||
pub user_id: String,
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
pub has_permission: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
/// 权限管理查询输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct PermissionFilterInput {
|
||||
pub resource: Option<String>,
|
||||
pub action: Option<String>,
|
||||
pub role_name: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
/// 角色权限分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignRolePermissionInput {
|
||||
pub role_name: String,
|
||||
pub permission_id: Uuid,
|
||||
pub granted_by: Uuid,
|
||||
}
|
||||
|
||||
/// 用户角色分配输入类型
|
||||
#[derive(Debug, InputObject)]
|
||||
pub struct AssignUserRoleInput {
|
||||
pub user_id: Uuid,
|
||||
pub role_name: String,
|
||||
pub granted_by: Uuid,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct PermissionPair {
|
||||
pub resource: String,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
impl Hash for PermissionPair {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.resource.hash(state);
|
||||
self.action.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PermissionPair {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.resource == other.resource && self.action == other.action
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PermissionPair {}
|
||||
130
src/graphql/types/users.rs
Normal file
130
src/graphql/types/users.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use crate::from_model;
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
|
||||
pub use input::*;
|
||||
pub use output::*;
|
||||
|
||||
pub mod input {
|
||||
use super::*;
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct RegisterInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub invite_code: String,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateUserInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct LoginInput {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateInviteCodeInput {
|
||||
pub expires_in_days: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct ValidateInviteCodeInput {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct InitializeAdminInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod output {
|
||||
use crate::from_model_with_mapping;
|
||||
|
||||
use super::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct InviteCodeResponse {
|
||||
pub code: String,
|
||||
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct InitializeAdminResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub user: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
pub struct UserInfoRespnose {
|
||||
pub total_users: i64,
|
||||
pub total_active_users: i64,
|
||||
pub total_inactive_users: i64,
|
||||
pub total_admin_users: i64,
|
||||
pub total_user_users: i64,
|
||||
pub users: Vec<UserWithGroups>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub is_activate: bool,
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// 包含组信息的用户
|
||||
#[derive(Debug, Clone, SimpleObject)]
|
||||
pub struct UserWithGroups {
|
||||
pub user: User,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
from_model!(
|
||||
User,
|
||||
crate::models::user::User,
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
is_activate,
|
||||
created_at,
|
||||
updated_at
|
||||
);
|
||||
|
||||
from_model_with_mapping!(
|
||||
UserInfoRespnose,
|
||||
crate::models::user::UserInfoRespnose,
|
||||
total_users,
|
||||
total_active_users,
|
||||
total_inactive_users,
|
||||
total_admin_users,
|
||||
total_user_users,
|
||||
users => |val: Vec<crate::models::user::UserWithGroups>| val.into_iter().map(|u| u.into()).collect(),
|
||||
);
|
||||
|
||||
from_model_with_mapping!(
|
||||
UserWithGroups,
|
||||
crate::models::user::UserWithGroups,
|
||||
user => |user: crate::models::user::User| user.into(),
|
||||
groups ,
|
||||
);
|
||||
}
|
||||
@ -12,9 +12,8 @@ use app::create_router;
|
||||
use clap::Parser;
|
||||
use cli::{
|
||||
AddPolicyArgs, AssignRoleArgs, BlogArgs, BlogCommands, CheckPermissionArgs, Cli, Commands,
|
||||
CreateBlogArgs, CreateCategoryArgs, CreateTagArgs, DeleteBlogArgs, ListBlogArgs,
|
||||
ListRolePermissionsArgs, ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands,
|
||||
RemovePolicyArgs, RemoveRoleArgs, ServeArgs, ShowBlogArgs, UpdateBlogArgs,
|
||||
RemovePolicyArgs, RemoveRoleArgs, ServeArgs,
|
||||
};
|
||||
use config::Config;
|
||||
use db::{create_pool, run_migrations};
|
||||
@ -23,6 +22,8 @@ use rustls;
|
||||
use std::process;
|
||||
use tokio::task;
|
||||
|
||||
use crate::graphql::types::PaginationInput;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
@ -512,8 +513,7 @@ fn print_config_info(config: &Config, args: &ServeArgs) {
|
||||
}
|
||||
|
||||
async fn blog_command(args: BlogArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use models::blog::*;
|
||||
use models::page_block::PaginationInput;
|
||||
use crate::graphql::types::blog::*;
|
||||
use serde_json;
|
||||
use services::blog_service::BlogService;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use async_graphql::{InputObject, SimpleObject};
|
||||
pub use crate::graphql::types::blog::input::*;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 博客分类模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct BlogCategory {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@ -22,7 +22,7 @@ pub struct BlogCategory {
|
||||
}
|
||||
|
||||
/// 博客标签模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct BlogTag {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
@ -37,7 +37,7 @@ pub struct BlogTag {
|
||||
}
|
||||
|
||||
/// 博客文章模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Blog {
|
||||
pub id: Uuid,
|
||||
pub title: String,
|
||||
@ -60,16 +60,15 @@ pub struct Blog {
|
||||
}
|
||||
|
||||
/// 博客文章详情(包含分类和标签信息)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlogDetail {
|
||||
#[graphql(flatten)]
|
||||
pub blog: Blog,
|
||||
pub category: Option<BlogCategory>,
|
||||
pub tags: Vec<BlogTag>,
|
||||
}
|
||||
|
||||
/// 博客标签关联模型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct BlogTagRelation {
|
||||
pub id: Uuid,
|
||||
pub blog_id: Uuid,
|
||||
@ -77,126 +76,8 @@ pub struct BlogTagRelation {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// 创建博客分类输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogCategoryInput {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
// 更新博客分类输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogCategoryInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
// 创建博客标签输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogTagInput {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 更新博客标签输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogTagInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
}
|
||||
|
||||
// 创建博客文章输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct CreateBlogInput {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub excerpt: Option<String>,
|
||||
pub content: serde_json::Value,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub featured_image: Option<String>,
|
||||
pub meta_title: Option<String>,
|
||||
pub meta_description: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
// 更新博客文章输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct UpdateBlogInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub excerpt: Option<String>,
|
||||
pub content: Option<serde_json::Value>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub featured_image: Option<String>,
|
||||
pub meta_title: Option<String>,
|
||||
pub meta_description: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
// 博客过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogFilterInput {
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub category_id: Option<Uuid>,
|
||||
pub status: Option<String>,
|
||||
pub is_featured: Option<bool>,
|
||||
pub is_active: Option<bool>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub search: Option<String>,
|
||||
pub date_from: Option<DateTime<Utc>>,
|
||||
pub date_to: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// 博客分类过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogCategoryFilterInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// 博客标签过滤器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogTagFilterInput {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub is_active: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
// 博客排序
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct BlogSortInput {
|
||||
pub field: String,
|
||||
pub direction: String, // "asc" or "desc"
|
||||
}
|
||||
|
||||
// 博客统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlogStats {
|
||||
pub total_blogs: i64,
|
||||
pub published_blogs: i64,
|
||||
|
||||
1
src/models/config.rs
Normal file
1
src/models/config.rs
Normal file
@ -0,0 +1 @@
|
||||
pub use crate::graphql::types::config::*;
|
||||
@ -1,13 +1,34 @@
|
||||
pub mod blog;
|
||||
pub mod config;
|
||||
pub mod invite_code;
|
||||
pub mod kafka_message;
|
||||
pub mod page_block;
|
||||
pub mod settings;
|
||||
pub mod user;
|
||||
|
||||
pub use blog::*;
|
||||
pub use config::*;
|
||||
pub use invite_code::*;
|
||||
pub use kafka_message::*;
|
||||
pub use page_block::*;
|
||||
pub use settings::*;
|
||||
pub use user::*;
|
||||
|
||||
pub struct PaginatedResult<T: Send + Sync> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub per_page: i32,
|
||||
pub total_pages: i32,
|
||||
}
|
||||
|
||||
impl<T: Send + Sync> PaginatedResult<T> {
|
||||
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
|
||||
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
|
||||
Self {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -578,44 +578,3 @@ pub struct BlockSortInput {
|
||||
pub field: String,
|
||||
pub direction: String, // "asc" or "desc"
|
||||
}
|
||||
|
||||
// 分页输入
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, InputObject)]
|
||||
pub struct PaginationInput {
|
||||
pub page: Option<i32>,
|
||||
pub per_page: Option<i32>,
|
||||
}
|
||||
|
||||
// 分页结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PaginatedResult<T: async_graphql::OutputType + Send + Sync> {
|
||||
pub items: Vec<T>,
|
||||
pub total: i64,
|
||||
pub page: i32,
|
||||
pub per_page: i32,
|
||||
pub total_pages: i32,
|
||||
}
|
||||
|
||||
impl<T: async_graphql::OutputType + Send + Sync> PaginatedResult<T> {
|
||||
pub fn new(items: Vec<T>, total: i64, page: i32, per_page: i32) -> Self {
|
||||
let total_pages = ((total as f64) / (per_page as f64)).ceil() as i32;
|
||||
Self {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
total_pages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)]
|
||||
pub struct PageStats {
|
||||
pub text_blocks: i32,
|
||||
pub chart_blocks: i32,
|
||||
pub settings_blocks: i32,
|
||||
pub table_blocks: i32,
|
||||
pub hero_blocks: i32,
|
||||
pub total_blocks: i32,
|
||||
}
|
||||
|
||||
@ -1,161 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{de::Error, Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Setting {
|
||||
pub id: Uuid,
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Option<Uuid>,
|
||||
pub updated_by: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateSetting {
|
||||
pub key: String,
|
||||
pub value: Option<String>,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: String,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateSetting {
|
||||
pub value: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub is_editable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingValue {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
pub value_type: String,
|
||||
pub description: Option<String>,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingFilter {
|
||||
pub category: Option<String>,
|
||||
pub is_system: Option<bool>,
|
||||
pub is_editable: Option<bool>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingHistory {
|
||||
pub id: Uuid,
|
||||
pub setting_id: Uuid,
|
||||
pub old_value: Option<String>,
|
||||
pub new_value: Option<String>,
|
||||
pub changed_by: Option<Uuid>,
|
||||
pub change_reason: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Setting {
|
||||
/// 获取配置值的类型化版本
|
||||
pub fn get_typed_value<T>(&self) -> Result<T, serde_json::Error>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
match &self.value {
|
||||
Some(v) => serde_json::from_str(v),
|
||||
None => Err(serde_json::Error::custom("No value set")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置配置值
|
||||
pub fn set_value<T>(&mut self, value: &T) -> Result<(), serde_json::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
self.value = Some(serde_json::to_string(value)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查是否为特定类型
|
||||
pub fn is_type(&self, expected_type: &str) -> bool {
|
||||
self.value_type == expected_type
|
||||
}
|
||||
|
||||
/// 获取布尔值
|
||||
pub fn get_bool(&self) -> Result<bool, String> {
|
||||
if self.value_type == "boolean" {
|
||||
self.value
|
||||
.as_ref()
|
||||
.and_then(|v| v.parse::<bool>().ok())
|
||||
.ok_or_else(|| "Invalid boolean value".to_string())
|
||||
} else {
|
||||
Err("Setting is not a boolean type".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数字值
|
||||
pub fn get_number(&self) -> Result<f64, String> {
|
||||
if self.value_type == "number" {
|
||||
self.value
|
||||
.as_ref()
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.ok_or_else(|| "Invalid number value".to_string())
|
||||
} else {
|
||||
Err("Setting is not a number type".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取JSON值
|
||||
pub fn get_json(&self) -> Result<serde_json::Value, serde_json::Error> {
|
||||
if self.value_type == "json" {
|
||||
match &self.value {
|
||||
Some(v) => serde_json::from_str(v),
|
||||
None => Ok(serde_json::Value::Null),
|
||||
}
|
||||
} else {
|
||||
Err(serde_json::Error::custom("Setting is not a JSON type"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取字符串值
|
||||
pub fn get_string(&self) -> Result<String, String> {
|
||||
if self.value_type == "string" {
|
||||
Ok(self.value.clone().unwrap_or_default())
|
||||
} else {
|
||||
Err("Setting is not a string type".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Setting {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Uuid::nil(),
|
||||
key: String::new(),
|
||||
value: None,
|
||||
value_type: "string".to_string(),
|
||||
description: None,
|
||||
category: Some("general".to_string()),
|
||||
is_encrypted: Some(false),
|
||||
is_system: Some(false),
|
||||
is_editable: Some(true),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
pub use crate::graphql::types::users::input::*;
|
||||
use async_graphql::{Enum, SimpleObject};
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_query::Iden;
|
||||
@ -18,13 +19,12 @@ impl Default for Role {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow, SimpleObject)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
#[graphql(skip)]
|
||||
pub password_hash: String,
|
||||
pub password_hash: Option<String>,
|
||||
pub role: Role,
|
||||
pub invite_code_id: Option<Uuid>,
|
||||
pub is_activate: bool,
|
||||
@ -32,27 +32,14 @@ pub struct User {
|
||||
pub updated_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateUserInput {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub invite_code: String,
|
||||
pub role: Option<Role>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LoginInput {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Iden, PartialEq, Eq)]
|
||||
pub enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Email,
|
||||
PasswordHash,
|
||||
InviteCodeId,
|
||||
Role,
|
||||
IsActivate,
|
||||
CreatedAt,
|
||||
@ -68,6 +55,7 @@ impl TryFrom<String> for Users {
|
||||
"username" => Ok(Users::Username),
|
||||
"email" => Ok(Users::Email),
|
||||
"role" => Ok(Users::Role),
|
||||
"invite_code_id" => Ok(Users::InviteCodeId),
|
||||
"is_activate" => Ok(Users::IsActivate),
|
||||
"created_at" => Ok(Users::CreatedAt),
|
||||
"updated_at" => Ok(Users::UpdatedAt),
|
||||
@ -76,13 +64,19 @@ impl TryFrom<String> for Users {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, SimpleObject)]
|
||||
pub struct UserInfoRow {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub role: Role,
|
||||
pub is_activate: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserInfoRespnose {
|
||||
pub total_users: i64,
|
||||
pub total_active_users: i64,
|
||||
pub total_inactive_users: i64,
|
||||
pub total_admin_users: i64,
|
||||
pub total_user_users: i64,
|
||||
pub users: Vec<UserWithGroups>,
|
||||
}
|
||||
|
||||
/// 包含组信息的用户
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserWithGroups {
|
||||
pub user: User,
|
||||
pub groups: Vec<String>,
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
use crate::graphql::types::PaginationInput;
|
||||
use crate::models::{blog::*, PaginatedResult};
|
||||
use crate::services::query_builder::DynamicQueryBuilder;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Utc;
|
||||
use sea_query::{extension::postgres::PgExpr, Expr, Iden, Order, PostgresQueryBuilder, Query};
|
||||
@ -5,10 +8,6 @@ use sea_query_binder::SqlxBinder;
|
||||
use sqlx::{FromRow, PgPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::blog::*;
|
||||
use crate::models::page_block::{PaginatedResult, PaginationInput};
|
||||
use crate::services::query_builder::DynamicQueryBuilder;
|
||||
|
||||
/// 博客相关表的列枚举
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum BlogsIden {
|
||||
|
||||
@ -198,6 +198,27 @@ impl CasbinService {
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_user_permissions(&self, user_id: &str) -> Result<Vec<(String, String)>> {
|
||||
let enforcer = self.enforcer.read().await;
|
||||
|
||||
let roles = enforcer.get_roles_for_user(user_id, None);
|
||||
|
||||
use std::collections::HashSet;
|
||||
let mut all_permissions = HashSet::new();
|
||||
|
||||
for role in roles {
|
||||
let policies = enforcer.get_filtered_policy(0, vec![role.to_string()]);
|
||||
|
||||
for policy in policies {
|
||||
if policy.len() >= 3 {
|
||||
all_permissions.insert((policy[1].to_string(), policy[2].to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_permissions.into_iter().collect())
|
||||
}
|
||||
|
||||
/// 重新加载策略
|
||||
pub async fn reload_policy(&self) -> Result<()> {
|
||||
let mut enforcer = self.enforcer.write().await;
|
||||
|
||||
@ -1,32 +1,89 @@
|
||||
use crate::models::Setting;
|
||||
use crate::services::settings_service::SettingsService;
|
||||
use crate::models::config::Config;
|
||||
use crate::services::config_service::ConfigsService;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 配置管理器,提供类型安全的配置访问和缓存
|
||||
pub struct SettingsManager {
|
||||
settings_service: Arc<SettingsService>,
|
||||
cache: Arc<RwLock<HashMap<String, Setting>>>,
|
||||
pub mod keys {
|
||||
// 应用配置
|
||||
pub const SITE_NAME: &str = "site.name";
|
||||
pub const SITE_DESCRIPTION: &str = "site.description";
|
||||
pub const SITE_KEYWORDS: &str = "site.keywords";
|
||||
pub const SITE_URL: &str = "site.url";
|
||||
pub const SITE_LOGO: &str = "site.logo";
|
||||
pub const SITE_COPYRIGHT: &str = "site.copyright";
|
||||
pub const SITE_ICP: &str = "site.icp";
|
||||
pub const SITE_ICP_URL: &str = "site.icp_url";
|
||||
pub const SITE_COLOR_STYLE: &str = "site.color_style";
|
||||
|
||||
// User
|
||||
pub const USER_DEFAULT_AVATAR: &str = "user.default_avatar";
|
||||
pub const USER_DEFAULT_ROLE: &str = "user.default_role";
|
||||
pub const USER_NEED_REGISTER_INVITE_CODE: &str = "user.register_invite_code";
|
||||
pub const USER_NEED_REGISTER_EMAIL_VERIFICATION: &str = "user.register_email_verification";
|
||||
pub const OPEN_LOGIN: &str = "user.open_login";
|
||||
pub const OPEN_RESET_PASSWORD: &str = "user.open_reset_password";
|
||||
|
||||
// EMAIL
|
||||
pub const EMAIL_SMTP_HOST: &str = "email.smtp_host";
|
||||
pub const EMAIL_SMTP_PORT: &str = "email.smtp_port";
|
||||
pub const EMAIL_SMTP_USER: &str = "email.smtp_user";
|
||||
pub const EMAIL_SMTP_PASSWORD: &str = "email.smtp_password";
|
||||
pub const EMAIL_SMTP_FROM: &str = "email.smtp_from";
|
||||
pub const EMAIL_SMTP_FROM_NAME: &str = "email.smtp_from_name";
|
||||
pub const EMAIL_SMTP_FROM_EMAIL: &str = "email.smtp_from_email";
|
||||
pub const EMAIL_SYSTEM_TEMPLATE: &str = "email.system_template";
|
||||
|
||||
// Blog
|
||||
pub const BLOG_DEFAULT_AUTHOR: &str = "blog.default_author";
|
||||
pub const BLOG_DEFAULT_CATEGORY: &str = "blog.default_category";
|
||||
pub const BLOG_DEFAULT_TAG: &str = "blog.default_tag";
|
||||
pub const BLOG_OPEN_COMMENT: &str = "blog.open_comment";
|
||||
|
||||
// 日志配置
|
||||
pub const LOGGING_LEVEL: &str = "logging.level";
|
||||
pub const LOGGING_MAX_FILES: &str = "logging.max_files";
|
||||
pub const LOGGING_MAX_FILE_SIZE: &str = "logging.max_file_size";
|
||||
|
||||
// 缓存配置
|
||||
pub const CACHE_TTL: &str = "cache.ttl";
|
||||
pub const CACHE_MAX_SIZE: &str = "cache.max_size";
|
||||
|
||||
// Switches
|
||||
pub const SWITCH_OPEN_REGISTER: &str = "switch.open_register";
|
||||
pub const SWITCH_OPEN_LOGIN: &str = "switch.open_login";
|
||||
pub const SWITCH_OPEN_RESET_PASSWORD: &str = "switch.open_reset_password";
|
||||
pub const SWITCH_OPEN_COMMENT: &str = "switch.open_comment";
|
||||
pub const SWITCH_OPEN_LIKE: &str = "switch.open_like";
|
||||
pub const SWITCH_OPEN_SHARE: &str = "switch.open_share";
|
||||
pub const SWITCH_OPEN_VIEW: &str = "switch.open_view";
|
||||
}
|
||||
|
||||
pub struct ConfigsManager {
|
||||
configs_service: Arc<ConfigsService>,
|
||||
cache: Arc<RwLock<HashMap<String, Config>>>,
|
||||
cache_ttl: std::time::Duration,
|
||||
last_cache_update: Arc<RwLock<std::time::Instant>>,
|
||||
}
|
||||
|
||||
impl SettingsManager {
|
||||
pub fn new(settings_service: SettingsService) -> Self {
|
||||
Self {
|
||||
settings_service: Arc::new(settings_service),
|
||||
impl ConfigsManager {
|
||||
pub async fn new(configs_service: ConfigsService) -> Self {
|
||||
let manager = Self {
|
||||
configs_service: Arc::new(configs_service),
|
||||
cache: Arc::new(RwLock::new(HashMap::new())),
|
||||
cache_ttl: std::time::Duration::from_secs(300), // 5分钟缓存
|
||||
last_cache_update: Arc::new(RwLock::new(std::time::Instant::now())),
|
||||
}
|
||||
};
|
||||
|
||||
manager.refresh_cache().await.unwrap();
|
||||
manager
|
||||
}
|
||||
|
||||
/// 刷新缓存
|
||||
async fn refresh_cache(&self) -> Result<()> {
|
||||
let settings = self.settings_service.get_all_settings().await?;
|
||||
let settings = self.configs_service.get_all_configs().await?;
|
||||
let mut cache = self.cache.write().await;
|
||||
cache.clear();
|
||||
|
||||
@ -47,7 +104,7 @@ impl SettingsManager {
|
||||
}
|
||||
|
||||
/// 获取配置值(带缓存)
|
||||
async fn get_cached_setting(&self, key: &str) -> Result<Option<Setting>> {
|
||||
async fn get_cached_setting(&self, key: &str) -> Result<Option<Config>> {
|
||||
if self.should_refresh_cache().await {
|
||||
self.refresh_cache().await?;
|
||||
}
|
||||
@ -144,14 +201,15 @@ impl SettingsManager {
|
||||
T: Serialize,
|
||||
{
|
||||
// 更新数据库
|
||||
if let Some(setting) = self.settings_service.get_setting_by_key(key).await? {
|
||||
let update_setting = crate::models::UpdateSetting {
|
||||
if let Some(setting) = self.configs_service.get_config_by_key(key).await? {
|
||||
let update_setting = crate::models::UpdateConfig {
|
||||
key: key.to_string(),
|
||||
value: Some(serde_json::to_string(value)?),
|
||||
description: None,
|
||||
category: None,
|
||||
is_editable: None,
|
||||
};
|
||||
self.settings_service
|
||||
self.configs_service
|
||||
.update_setting(setting.id, update_setting, uuid::Uuid::nil())
|
||||
.await?;
|
||||
}
|
||||
@ -165,7 +223,7 @@ impl SettingsManager {
|
||||
/// 批量设置配置值
|
||||
pub async fn set_values(&self, updates: HashMap<String, serde_json::Value>) -> Result<()> {
|
||||
let updates: Vec<(String, serde_json::Value)> = updates.into_iter().collect();
|
||||
self.settings_service
|
||||
self.configs_service
|
||||
.batch_update_settings(updates, uuid::Uuid::nil())
|
||||
.await?;
|
||||
|
||||
@ -181,7 +239,7 @@ impl SettingsManager {
|
||||
}
|
||||
|
||||
/// 获取所有配置
|
||||
pub async fn get_all_settings(&self) -> Result<Vec<Setting>> {
|
||||
pub async fn get_all_settings(&self) -> Result<Vec<Config>> {
|
||||
if self.should_refresh_cache().await {
|
||||
self.refresh_cache().await?;
|
||||
}
|
||||
@ -191,7 +249,7 @@ impl SettingsManager {
|
||||
}
|
||||
|
||||
/// 获取分类配置
|
||||
pub async fn get_settings_by_category(&self, category: &str) -> Result<Vec<Setting>> {
|
||||
pub async fn get_settings_by_category(&self, category: &str) -> Result<Vec<Config>> {
|
||||
let all_settings = self.get_all_settings().await?;
|
||||
Ok(all_settings
|
||||
.into_iter()
|
||||
@ -205,106 +263,7 @@ impl SettingsManager {
|
||||
}
|
||||
|
||||
/// 获取配置元数据
|
||||
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Setting>> {
|
||||
pub async fn get_setting_metadata(&self, key: &str) -> Result<Option<Config>> {
|
||||
self.get_cached_setting(key).await
|
||||
}
|
||||
}
|
||||
|
||||
/// 预定义的配置键常量
|
||||
pub mod keys {
|
||||
// 应用配置
|
||||
pub const APP_NAME: &str = "app.name";
|
||||
pub const APP_VERSION: &str = "app.version";
|
||||
pub const APP_DEBUG: &str = "app.debug";
|
||||
pub const APP_TIMEZONE: &str = "app.timezone";
|
||||
|
||||
// 数据库配置
|
||||
pub const DB_MAX_CONNECTIONS: &str = "database.max_connections";
|
||||
pub const DB_CONNECTION_TIMEOUT: &str = "database.connection_timeout";
|
||||
|
||||
// Kafka配置
|
||||
pub const KAFKA_MAX_RETRIES: &str = "kafka.max_retries";
|
||||
pub const KAFKA_RETRY_DELAY: &str = "kafka.retry_delay";
|
||||
|
||||
// 安全配置
|
||||
pub const SECURITY_SESSION_TIMEOUT: &str = "security.session_timeout";
|
||||
pub const SECURITY_MAX_LOGIN_ATTEMPTS: &str = "security.max_login_attempts";
|
||||
|
||||
// 日志配置
|
||||
pub const LOGGING_LEVEL: &str = "logging.level";
|
||||
pub const LOGGING_MAX_FILES: &str = "logging.max_files";
|
||||
|
||||
// 缓存配置
|
||||
pub const CACHE_TTL: &str = "cache.ttl";
|
||||
pub const CACHE_MAX_SIZE: &str = "cache.max_size";
|
||||
}
|
||||
|
||||
/// 配置验证器
|
||||
pub struct SettingsValidator;
|
||||
|
||||
impl SettingsValidator {
|
||||
/// 验证必需的配置项
|
||||
pub async fn validate_required_settings(manager: &SettingsManager) -> Result<Vec<String>> {
|
||||
let required_keys = vec![
|
||||
keys::APP_NAME,
|
||||
keys::APP_VERSION,
|
||||
keys::DB_MAX_CONNECTIONS,
|
||||
keys::KAFKA_MAX_RETRIES,
|
||||
];
|
||||
|
||||
let mut missing = Vec::new();
|
||||
|
||||
for key in required_keys {
|
||||
if !manager.has_setting(key).await? {
|
||||
missing.push(key.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(missing)
|
||||
}
|
||||
|
||||
/// 验证配置值类型
|
||||
pub async fn validate_setting_types(manager: &SettingsManager) -> Result<Vec<String>> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// 验证数字类型配置
|
||||
let number_keys = vec![
|
||||
keys::DB_MAX_CONNECTIONS,
|
||||
keys::DB_CONNECTION_TIMEOUT,
|
||||
keys::KAFKA_MAX_RETRIES,
|
||||
keys::KAFKA_RETRY_DELAY,
|
||||
keys::SECURITY_SESSION_TIMEOUT,
|
||||
keys::SECURITY_MAX_LOGIN_ATTEMPTS,
|
||||
keys::LOGGING_MAX_FILES,
|
||||
keys::CACHE_TTL,
|
||||
keys::CACHE_MAX_SIZE,
|
||||
];
|
||||
|
||||
for key in number_keys {
|
||||
if let Some(setting) = manager.get_setting_metadata(key).await? {
|
||||
if setting.value_type != "number" {
|
||||
errors.push(format!(
|
||||
"{} should be number type, got {}",
|
||||
key, setting.value_type
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证布尔类型配置
|
||||
let bool_keys = vec![keys::APP_DEBUG];
|
||||
|
||||
for key in bool_keys {
|
||||
if let Some(setting) = manager.get_setting_metadata(key).await? {
|
||||
if setting.value_type != "boolean" {
|
||||
errors.push(format!(
|
||||
"{} should be boolean type, got {}",
|
||||
key, setting.value_type
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(errors)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::models::{CreateSetting, Setting, SettingFilter, SettingHistory, UpdateSetting};
|
||||
use crate::models::config::*;
|
||||
use crate::services::query_builder::DynamicQueryBuilder;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
@ -11,7 +11,7 @@ use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Iden, Clone)]
|
||||
enum Settings {
|
||||
enum Configs {
|
||||
Table,
|
||||
Id,
|
||||
Key,
|
||||
@ -29,10 +29,10 @@ enum Settings {
|
||||
}
|
||||
|
||||
#[derive(Iden, Clone)]
|
||||
enum SettingsHistory {
|
||||
enum ConfigsHistory {
|
||||
Table,
|
||||
Id,
|
||||
SettingId,
|
||||
ConfigId,
|
||||
OldValue,
|
||||
NewValue,
|
||||
ChangedBy,
|
||||
@ -40,12 +40,12 @@ enum SettingsHistory {
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
pub struct SettingsService {
|
||||
pub struct ConfigsService {
|
||||
pool: PgPool,
|
||||
query_builder: DynamicQueryBuilder,
|
||||
}
|
||||
|
||||
impl SettingsService {
|
||||
impl ConfigsService {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
let query_builder = DynamicQueryBuilder::new(pool.clone());
|
||||
Self {
|
||||
@ -54,97 +54,13 @@ impl SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取数据库连接池的引用(用于事务)
|
||||
pub fn get_pool(&self) -> &PgPool {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// 创建新的配置项
|
||||
pub async fn create_setting(
|
||||
&self,
|
||||
create_setting: CreateSetting,
|
||||
user_id: Uuid,
|
||||
) -> Result<Setting> {
|
||||
// 检查key是否已存在
|
||||
let existing = sqlx::query!("SELECT id FROM settings WHERE key = $1", create_setting.key)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
if existing.is_some() {
|
||||
return Err(anyhow!(
|
||||
"Setting with key '{}' already exists",
|
||||
create_setting.key
|
||||
));
|
||||
}
|
||||
|
||||
let setting = sqlx::query_as!(
|
||||
Setting,
|
||||
r#"
|
||||
INSERT INTO settings (key, value, value_type, description, category, is_encrypted, is_system, is_editable, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
created_by, updated_by
|
||||
"#,
|
||||
create_setting.key,
|
||||
create_setting.value,
|
||||
create_setting.value_type,
|
||||
create_setting.description,
|
||||
create_setting.category,
|
||||
create_setting.is_encrypted.unwrap_or(false),
|
||||
create_setting.is_system.unwrap_or(false),
|
||||
create_setting.is_editable.unwrap_or(true),
|
||||
user_id
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(setting)
|
||||
}
|
||||
|
||||
/// 根据key获取配置项
|
||||
pub async fn get_setting_by_key(&self, key: &str) -> Result<Option<Setting>> {
|
||||
let setting = sqlx::query_as!(
|
||||
Setting,
|
||||
r#"
|
||||
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
created_by, updated_by
|
||||
FROM settings WHERE key = $1
|
||||
"#,
|
||||
key
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(setting)
|
||||
}
|
||||
|
||||
/// 根据ID获取配置项
|
||||
pub async fn get_setting_by_id(&self, id: Uuid) -> Result<Option<Setting>> {
|
||||
let setting = sqlx::query_as!(
|
||||
Setting,
|
||||
r#"
|
||||
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
created_by, updated_by
|
||||
FROM settings WHERE id = $1
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(setting)
|
||||
}
|
||||
|
||||
/// 获取所有配置项
|
||||
pub async fn get_all_settings(&self) -> Result<Vec<Setting>> {
|
||||
pub async fn get_all_configs(&self) -> Result<Vec<Config>> {
|
||||
let settings = sqlx::query_as!(
|
||||
Setting,
|
||||
Config,
|
||||
r#"
|
||||
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
@ -159,53 +75,52 @@ impl SettingsService {
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// 根据过滤条件获取配置项 (使用 sea-query 构建动态查询)
|
||||
pub async fn get_settings_with_filter(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
|
||||
pub async fn get_settings_with_filter(&self, filter: &ConfigFilter) -> Result<Vec<Config>> {
|
||||
let mut query = Query::select();
|
||||
query
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.from(Settings::Table);
|
||||
.from(Configs::Table);
|
||||
|
||||
// 动态添加过滤条件
|
||||
if let Some(category) = &filter.category {
|
||||
query.and_where(Expr::col(Settings::Category).eq(category));
|
||||
query.and_where(Expr::col(Configs::Category).eq(category));
|
||||
}
|
||||
|
||||
if let Some(is_system) = &filter.is_system {
|
||||
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
|
||||
query.and_where(Expr::col(Configs::IsSystem).eq(*is_system));
|
||||
}
|
||||
|
||||
if let Some(is_editable) = &filter.is_editable {
|
||||
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
|
||||
query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable));
|
||||
}
|
||||
|
||||
if let Some(search) = &filter.search {
|
||||
let search_pattern = format!("%{}%", search);
|
||||
query.and_where(
|
||||
Expr::col(Settings::Key)
|
||||
Expr::col(Configs::Key)
|
||||
.ilike(&search_pattern)
|
||||
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
|
||||
.or(Expr::col(Configs::Description).ilike(&search_pattern)),
|
||||
);
|
||||
}
|
||||
|
||||
// 添加排序
|
||||
query
|
||||
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||
.order_by(Settings::Key, sea_query::Order::Asc);
|
||||
.order_by(Configs::Category, sea_query::Order::Asc)
|
||||
.order_by(Configs::Key, sea_query::Order::Asc);
|
||||
|
||||
// 构建并执行查询
|
||||
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
|
||||
@ -213,7 +128,7 @@ impl SettingsService {
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -238,18 +153,18 @@ impl SettingsService {
|
||||
pub async fn update_setting(
|
||||
&self,
|
||||
id: Uuid,
|
||||
update_setting: UpdateSetting,
|
||||
update_setting: UpdateConfig,
|
||||
user_id: Uuid,
|
||||
) -> Result<Setting> {
|
||||
let setting = self.get_setting_by_id(id).await?;
|
||||
let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?;
|
||||
) -> Result<Config> {
|
||||
let setting = self.get_config_by_id(id).await?;
|
||||
let setting = setting.ok_or_else(|| anyhow!("Config not found"))?;
|
||||
|
||||
if !setting.is_editable.unwrap_or(false) {
|
||||
return Err(anyhow!("Setting is not editable"));
|
||||
return Err(anyhow!("Config is not editable"));
|
||||
}
|
||||
|
||||
let updated_setting = sqlx::query_as!(
|
||||
Setting,
|
||||
Config,
|
||||
r#"
|
||||
UPDATE settings
|
||||
SET value = COALESCE($1, value),
|
||||
@ -277,39 +192,58 @@ impl SettingsService {
|
||||
Ok(updated_setting)
|
||||
}
|
||||
|
||||
/// 删除配置项
|
||||
pub async fn delete_setting(&self, id: Uuid) -> Result<bool> {
|
||||
let setting = self.get_setting_by_id(id).await?;
|
||||
let setting = setting.ok_or_else(|| anyhow!("Setting not found"))?;
|
||||
|
||||
if setting.is_system.unwrap_or(false) {
|
||||
return Err(anyhow!("Cannot delete system settings"));
|
||||
}
|
||||
|
||||
let result = sqlx::query!("DELETE FROM settings WHERE id = $1", id)
|
||||
.execute(&self.pool)
|
||||
pub async fn get_config_by_id(&self, id: Uuid) -> Result<Option<Config>> {
|
||||
let config = sqlx::query_as!(
|
||||
Config,
|
||||
r#"
|
||||
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
created_by, updated_by
|
||||
FROM settings WHERE id = $1
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn get_config_by_key(&self, key: &str) -> Result<Option<Config>> {
|
||||
let config = sqlx::query_as!(
|
||||
Config,
|
||||
r#"
|
||||
SELECT id, key, value, value_type, description, category, is_encrypted, is_system, is_editable,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
created_by, updated_by
|
||||
FROM settings WHERE key = $1
|
||||
"#,
|
||||
key
|
||||
)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 批量更新配置项
|
||||
pub async fn batch_update_settings(
|
||||
&self,
|
||||
updates: Vec<(String, Value)>,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<Setting>> {
|
||||
) -> Result<Vec<Config>> {
|
||||
let mut updated_settings = Vec::new();
|
||||
|
||||
for (key, value) in updates {
|
||||
if let Some(setting) = self.get_setting_by_key(&key).await? {
|
||||
if let Some(setting) = self.get_config_by_key(&key).await? {
|
||||
if !setting.is_editable.unwrap_or(false) {
|
||||
continue; // 跳过不可编辑的配置
|
||||
}
|
||||
|
||||
let value_str = serde_json::to_string(&value)?;
|
||||
let updated_setting = sqlx::query_as!(
|
||||
Setting,
|
||||
Config,
|
||||
r#"
|
||||
UPDATE settings
|
||||
SET value = $1, updated_by = $2, updated_at = CURRENT_TIMESTAMP
|
||||
@ -334,9 +268,9 @@ impl SettingsService {
|
||||
}
|
||||
|
||||
/// 获取配置历史
|
||||
pub async fn get_setting_history(&self, setting_id: Uuid) -> Result<Vec<SettingHistory>> {
|
||||
pub async fn get_setting_history(&self, setting_id: Uuid) -> Result<Vec<ConfigHistory>> {
|
||||
let history = sqlx::query_as!(
|
||||
SettingHistory,
|
||||
ConfigHistory,
|
||||
r#"
|
||||
SELECT id, setting_id, old_value, new_value, changed_by, change_reason,
|
||||
created_at as "created_at: DateTime<Utc>"
|
||||
@ -401,42 +335,42 @@ impl SettingsService {
|
||||
}
|
||||
|
||||
/// 重置配置到默认值
|
||||
pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result<Vec<Setting>> {
|
||||
pub async fn reset_to_defaults(&self, user_id: Uuid) -> Result<Vec<Config>> {
|
||||
// 这里可以实现重置逻辑,比如从配置文件重新加载默认值
|
||||
// 暂时返回空列表
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// 导出配置 (使用 sea-query 构建动态查询)
|
||||
pub async fn export_settings(&self, category: Option<&str>) -> Result<Vec<Setting>> {
|
||||
pub async fn export_settings(&self, category: Option<&str>) -> Result<Vec<Config>> {
|
||||
let mut query = Query::select();
|
||||
query
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.from(Settings::Table);
|
||||
.from(Configs::Table);
|
||||
|
||||
// 根据分类过滤
|
||||
if let Some(cat) = category {
|
||||
query.and_where(Expr::col(Settings::Category).eq(cat));
|
||||
query.and_where(Expr::col(Configs::Category).eq(cat));
|
||||
}
|
||||
|
||||
// 添加排序
|
||||
query
|
||||
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||
.order_by(Settings::Key, sea_query::Order::Asc);
|
||||
.order_by(Configs::Category, sea_query::Order::Asc)
|
||||
.order_by(Configs::Key, sea_query::Order::Asc);
|
||||
|
||||
// 构建并执行查询
|
||||
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
|
||||
@ -444,7 +378,7 @@ impl SettingsService {
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -465,54 +399,54 @@ impl SettingsService {
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
/// 导入配置
|
||||
pub async fn import_settings(
|
||||
&self,
|
||||
settings: Vec<CreateSetting>,
|
||||
user_id: Uuid,
|
||||
) -> Result<Vec<Setting>> {
|
||||
let mut imported_settings = Vec::new();
|
||||
// /// 导入配置
|
||||
// pub async fn import_settings(
|
||||
// &self,
|
||||
// settings: Vec<CreateConfig>,
|
||||
// user_id: Uuid,
|
||||
// ) -> Result<Vec<Config>> {
|
||||
// let mut imported_settings = Vec::new();
|
||||
|
||||
for create_setting in settings {
|
||||
// 检查是否已存在
|
||||
if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? {
|
||||
// 如果存在且可编辑,则更新
|
||||
if existing.is_editable.unwrap_or(false) {
|
||||
let update_setting = UpdateSetting {
|
||||
value: create_setting.value,
|
||||
description: create_setting.description,
|
||||
category: Some(create_setting.category),
|
||||
is_editable: Some(create_setting.is_editable.unwrap_or(true)),
|
||||
};
|
||||
let updated = self
|
||||
.update_setting(existing.id, update_setting, user_id)
|
||||
.await?;
|
||||
imported_settings.push(updated);
|
||||
}
|
||||
} else {
|
||||
// 如果不存在,则创建
|
||||
let new_setting = self.create_setting(create_setting, user_id).await?;
|
||||
imported_settings.push(new_setting);
|
||||
}
|
||||
}
|
||||
// for create_setting in settings {
|
||||
// // 检查是否已存在
|
||||
// if let Some(existing) = self.get_setting_by_key(&create_setting.key).await? {
|
||||
// // 如果存在且可编辑,则更新
|
||||
// if existing.is_editable.unwrap_or(false) {
|
||||
// let update_setting = UpdateConfig {
|
||||
// value: create_setting.value,
|
||||
// description: create_setting.description,
|
||||
// category: Some(create_setting.category),
|
||||
// is_editable: Some(create_setting.is_editable.unwrap_or(true)),
|
||||
// };
|
||||
// let updated = self
|
||||
// .update_setting(existing.id, update_setting, user_id)
|
||||
// .await?;
|
||||
// imported_settings.push(updated);
|
||||
// }
|
||||
// } else {
|
||||
// // 如果不存在,则创建
|
||||
// let new_setting = self.create_setting(create_setting, user_id).await?;
|
||||
// imported_settings.push(new_setting);
|
||||
// }
|
||||
// }
|
||||
|
||||
Ok(imported_settings)
|
||||
}
|
||||
// Ok(imported_settings)
|
||||
// }
|
||||
|
||||
/// 分页查询配置项 (sea-query 分页示例)
|
||||
pub async fn get_settings_paginated(
|
||||
&self,
|
||||
filter: &SettingFilter,
|
||||
filter: &ConfigFilter,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> Result<(Vec<Setting>, u64)> {
|
||||
) -> Result<(Vec<Config>, u64)> {
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
// 查询总数
|
||||
let mut count_query = Query::select();
|
||||
count_query
|
||||
.expr(Expr::col(Settings::Id).count())
|
||||
.from(Settings::Table);
|
||||
.expr(Expr::col(Configs::Id).count())
|
||||
.from(Configs::Table);
|
||||
|
||||
// 应用过滤条件
|
||||
self.apply_filter_conditions(&mut count_query, filter);
|
||||
@ -526,27 +460,27 @@ impl SettingsService {
|
||||
let mut data_query = Query::select();
|
||||
data_query
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.from(Settings::Table);
|
||||
.from(Configs::Table);
|
||||
|
||||
self.apply_filter_conditions(&mut data_query, filter);
|
||||
|
||||
data_query
|
||||
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||
.order_by(Settings::Key, sea_query::Order::Asc)
|
||||
.order_by(Configs::Category, sea_query::Order::Asc)
|
||||
.order_by(Configs::Key, sea_query::Order::Asc)
|
||||
.limit(page_size)
|
||||
.offset(offset);
|
||||
|
||||
@ -555,7 +489,7 @@ impl SettingsService {
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -584,75 +518,75 @@ impl SettingsService {
|
||||
value_types: Option<Vec<String>>,
|
||||
is_system: Option<bool>,
|
||||
date_range: Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>,
|
||||
) -> Result<Vec<Setting>> {
|
||||
) -> Result<Vec<Config>> {
|
||||
let mut query = Query::select();
|
||||
query
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.from(Settings::Table);
|
||||
.from(Configs::Table);
|
||||
|
||||
// 搜索关键词
|
||||
if let Some(term) = search_term {
|
||||
let search_pattern = format!("%{}%", term);
|
||||
query.and_where(
|
||||
Expr::col(Settings::Key)
|
||||
Expr::col(Configs::Key)
|
||||
.ilike(&search_pattern)
|
||||
.or(Expr::col(Settings::Description).ilike(&search_pattern))
|
||||
.or(Expr::col(Settings::Value).ilike(&search_pattern)),
|
||||
.or(Expr::col(Configs::Description).ilike(&search_pattern))
|
||||
.or(Expr::col(Configs::Value).ilike(&search_pattern)),
|
||||
);
|
||||
}
|
||||
|
||||
// 多个分类
|
||||
if let Some(cats) = categories {
|
||||
if !cats.is_empty() {
|
||||
query.and_where(Expr::col(Settings::Category).is_in(cats));
|
||||
query.and_where(Expr::col(Configs::Category).is_in(cats));
|
||||
}
|
||||
}
|
||||
|
||||
// 多个值类型
|
||||
if let Some(types) = value_types {
|
||||
if !types.is_empty() {
|
||||
query.and_where(Expr::col(Settings::ValueType).is_in(types));
|
||||
query.and_where(Expr::col(Configs::ValueType).is_in(types));
|
||||
}
|
||||
}
|
||||
|
||||
// 系统设置
|
||||
if let Some(sys) = is_system {
|
||||
query.and_where(Expr::col(Settings::IsSystem).eq(sys));
|
||||
query.and_where(Expr::col(Configs::IsSystem).eq(sys));
|
||||
}
|
||||
|
||||
// 日期范围
|
||||
if let Some((start_date, end_date)) = date_range {
|
||||
query.and_where(
|
||||
Expr::col(Settings::CreatedAt)
|
||||
Expr::col(Configs::CreatedAt)
|
||||
.gte(start_date)
|
||||
.and(Expr::col(Settings::CreatedAt).lte(end_date)),
|
||||
.and(Expr::col(Configs::CreatedAt).lte(end_date)),
|
||||
);
|
||||
}
|
||||
|
||||
query
|
||||
.order_by(Settings::Category, sea_query::Order::Asc)
|
||||
.order_by(Settings::Key, sea_query::Order::Asc);
|
||||
.order_by(Configs::Category, sea_query::Order::Asc)
|
||||
.order_by(Configs::Key, sea_query::Order::Asc);
|
||||
|
||||
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
|
||||
let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?;
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -676,37 +610,37 @@ impl SettingsService {
|
||||
/// 批量操作 (动态更新多个配置项)
|
||||
pub async fn batch_update_by_conditions(
|
||||
&self,
|
||||
conditions: &SettingFilter,
|
||||
conditions: &ConfigFilter,
|
||||
updates: HashMap<String, serde_json::Value>,
|
||||
user_id: Uuid,
|
||||
) -> Result<u64> {
|
||||
// 构建更新查询
|
||||
let mut update_query = Query::update();
|
||||
update_query.table(Settings::Table);
|
||||
update_query.table(Configs::Table);
|
||||
|
||||
// 添加更新字段
|
||||
for (field, value) in updates {
|
||||
match field.as_str() {
|
||||
"description" => {
|
||||
if let Some(desc) = value.as_str() {
|
||||
update_query.value(Settings::Description, desc);
|
||||
update_query.value(Configs::Description, desc);
|
||||
}
|
||||
}
|
||||
"category" => {
|
||||
if let Some(cat) = value.as_str() {
|
||||
update_query.value(Settings::Category, cat);
|
||||
update_query.value(Configs::Category, cat);
|
||||
}
|
||||
}
|
||||
"is_editable" => {
|
||||
if let Some(editable) = value.as_bool() {
|
||||
update_query.value(Settings::IsEditable, editable);
|
||||
update_query.value(Configs::IsEditable, editable);
|
||||
}
|
||||
}
|
||||
_ => {} // 忽略未知字段
|
||||
}
|
||||
}
|
||||
|
||||
update_query.value(Settings::UpdatedBy, user_id);
|
||||
update_query.value(Configs::UpdatedBy, user_id);
|
||||
|
||||
// 应用过滤条件
|
||||
self.apply_filter_conditions(&mut update_query, conditions);
|
||||
@ -718,67 +652,67 @@ impl SettingsService {
|
||||
}
|
||||
|
||||
/// 辅助方法:应用过滤条件到查询
|
||||
fn apply_filter_conditions<T>(&self, query: &mut T, filter: &SettingFilter)
|
||||
fn apply_filter_conditions<T>(&self, query: &mut T, filter: &ConfigFilter)
|
||||
where
|
||||
T: sea_query::QueryStatementWriter + sea_query::ConditionalStatement,
|
||||
{
|
||||
if let Some(category) = &filter.category {
|
||||
query.and_where(Expr::col(Settings::Category).eq(category));
|
||||
query.and_where(Expr::col(Configs::Category).eq(category));
|
||||
}
|
||||
|
||||
if let Some(is_system) = &filter.is_system {
|
||||
query.and_where(Expr::col(Settings::IsSystem).eq(*is_system));
|
||||
query.and_where(Expr::col(Configs::IsSystem).eq(*is_system));
|
||||
}
|
||||
|
||||
if let Some(is_editable) = &filter.is_editable {
|
||||
query.and_where(Expr::col(Settings::IsEditable).eq(*is_editable));
|
||||
query.and_where(Expr::col(Configs::IsEditable).eq(*is_editable));
|
||||
}
|
||||
|
||||
if let Some(search) = &filter.search {
|
||||
let search_pattern = format!("%{}%", search);
|
||||
query.and_where(
|
||||
Expr::col(Settings::Key)
|
||||
Expr::col(Configs::Key)
|
||||
.ilike(&search_pattern)
|
||||
.or(Expr::col(Settings::Description).ilike(&search_pattern)),
|
||||
.or(Expr::col(Configs::Description).ilike(&search_pattern)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用查询构建器的简化过滤查询示例
|
||||
pub async fn get_settings_with_builder(&self, filter: &SettingFilter) -> Result<Vec<Setting>> {
|
||||
pub async fn get_settings_with_builder(&self, filter: &ConfigFilter) -> Result<Vec<Config>> {
|
||||
let rows = self
|
||||
.query_builder
|
||||
.select(Settings::Table)
|
||||
.select(Configs::Table)
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.condition_option(Settings::Category, filter.category.clone())
|
||||
.condition_option(Settings::IsSystem, filter.is_system)
|
||||
.condition_option(Settings::IsEditable, filter.is_editable)
|
||||
.condition_option(Configs::Category, filter.category.clone())
|
||||
.condition_option(Configs::IsSystem, filter.is_system)
|
||||
.condition_option(Configs::IsEditable, filter.is_editable)
|
||||
.search_like(
|
||||
vec![Settings::Key, Settings::Description],
|
||||
vec![Configs::Key, Configs::Description],
|
||||
filter.search.as_deref(),
|
||||
)
|
||||
.order_by(Settings::Category, Order::Asc)
|
||||
.order_by(Settings::Key, Order::Asc)
|
||||
.order_by(Configs::Category, Order::Asc)
|
||||
.order_by(Configs::Key, Order::Asc)
|
||||
.fetch_all()
|
||||
.await?;
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -802,19 +736,19 @@ impl SettingsService {
|
||||
/// 使用查询构建器的分页查询示例
|
||||
pub async fn get_settings_paginated_with_builder(
|
||||
&self,
|
||||
filter: &SettingFilter,
|
||||
filter: &ConfigFilter,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> Result<(Vec<Setting>, i64)> {
|
||||
) -> Result<(Vec<Config>, i64)> {
|
||||
// 获取总数
|
||||
let total_count = self
|
||||
.query_builder
|
||||
.select(Settings::Table)
|
||||
.condition_option(Settings::Category, filter.category.clone())
|
||||
.condition_option(Settings::IsSystem, filter.is_system)
|
||||
.condition_option(Settings::IsEditable, filter.is_editable)
|
||||
.select(Configs::Table)
|
||||
.condition_option(Configs::Category, filter.category.clone())
|
||||
.condition_option(Configs::IsSystem, filter.is_system)
|
||||
.condition_option(Configs::IsEditable, filter.is_editable)
|
||||
.search_like(
|
||||
vec![Settings::Key, Settings::Description],
|
||||
vec![Configs::Key, Configs::Description],
|
||||
filter.search.as_deref(),
|
||||
)
|
||||
.count()
|
||||
@ -823,38 +757,38 @@ impl SettingsService {
|
||||
// 获取分页数据
|
||||
let rows = self
|
||||
.query_builder
|
||||
.select(Settings::Table)
|
||||
.select(Configs::Table)
|
||||
.columns([
|
||||
Settings::Id,
|
||||
Settings::Key,
|
||||
Settings::Value,
|
||||
Settings::ValueType,
|
||||
Settings::Description,
|
||||
Settings::Category,
|
||||
Settings::IsEncrypted,
|
||||
Settings::IsSystem,
|
||||
Settings::IsEditable,
|
||||
Settings::CreatedAt,
|
||||
Settings::UpdatedAt,
|
||||
Settings::CreatedBy,
|
||||
Settings::UpdatedBy,
|
||||
Configs::Id,
|
||||
Configs::Key,
|
||||
Configs::Value,
|
||||
Configs::ValueType,
|
||||
Configs::Description,
|
||||
Configs::Category,
|
||||
Configs::IsEncrypted,
|
||||
Configs::IsSystem,
|
||||
Configs::IsEditable,
|
||||
Configs::CreatedAt,
|
||||
Configs::UpdatedAt,
|
||||
Configs::CreatedBy,
|
||||
Configs::UpdatedBy,
|
||||
])
|
||||
.condition_option(Settings::Category, filter.category.clone())
|
||||
.condition_option(Settings::IsSystem, filter.is_system)
|
||||
.condition_option(Settings::IsEditable, filter.is_editable)
|
||||
.condition_option(Configs::Category, filter.category.clone())
|
||||
.condition_option(Configs::IsSystem, filter.is_system)
|
||||
.condition_option(Configs::IsEditable, filter.is_editable)
|
||||
.search_like(
|
||||
vec![Settings::Key, Settings::Description],
|
||||
vec![Configs::Key, Configs::Description],
|
||||
filter.search.as_deref(),
|
||||
)
|
||||
.order_by(Settings::Category, Order::Asc)
|
||||
.order_by(Settings::Key, Order::Asc)
|
||||
.order_by(Configs::Category, Order::Asc)
|
||||
.order_by(Configs::Key, Order::Asc)
|
||||
.paginate(page, page_size)
|
||||
.fetch_all()
|
||||
.await?;
|
||||
|
||||
let mut settings = Vec::new();
|
||||
for row in rows {
|
||||
let setting = Setting {
|
||||
let setting = Config {
|
||||
id: row.get("id"),
|
||||
key: row.get("key"),
|
||||
value: row.get("value"),
|
||||
@ -1,10 +1,9 @@
|
||||
pub mod blog_service;
|
||||
pub mod casbin_service;
|
||||
pub mod config_manager;
|
||||
pub mod config_service;
|
||||
pub mod invite_code_service;
|
||||
pub mod mosaic_service;
|
||||
pub mod page_block_service;
|
||||
pub mod query_builder;
|
||||
pub mod settings_manager;
|
||||
pub mod settings_service;
|
||||
pub mod system_config_service;
|
||||
pub mod user_service;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
use argon2::password_hash::{rand_core::OsRng, SaltString};
|
||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, SaltString},
|
||||
Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
|
||||
};
|
||||
use async_graphql::{Error, Result};
|
||||
use sea_query::{Expr, PostgresQueryBuilder};
|
||||
use sea_query_binder::SqlxBinder;
|
||||
@ -8,10 +10,9 @@ use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::Claims;
|
||||
use crate::graphql::types::{
|
||||
CreateUserInput, LoginInput, LoginResponse, RegisterInput, UserInfoRespnose,
|
||||
};
|
||||
use crate::models::user::{Role, User};
|
||||
use crate::graphql::types::users::LoginResponse;
|
||||
use crate::models::user::*;
|
||||
use crate::services::casbin_service::CasbinService;
|
||||
|
||||
use crate::services::invite_code_service::InviteCodeService;
|
||||
use crate::services::system_config_service::SystemConfigService;
|
||||
@ -19,16 +20,51 @@ use crate::services::system_config_service::SystemConfigService;
|
||||
pub struct UserService {
|
||||
pool: PgPool,
|
||||
jwt_secret: String,
|
||||
casbin_service: Option<CasbinService>,
|
||||
}
|
||||
|
||||
impl UserService {
|
||||
pub fn new(pool: PgPool, jwt_secret: String) -> Self {
|
||||
Self { pool, jwt_secret }
|
||||
Self {
|
||||
pool,
|
||||
jwt_secret,
|
||||
casbin_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_casbin(mut self, casbin_service: CasbinService) -> Self {
|
||||
self.casbin_service = Some(casbin_service);
|
||||
self
|
||||
}
|
||||
|
||||
/// 获取用户的组(角色)
|
||||
pub async fn get_user_groups(&self, user_id: &Uuid) -> Result<Vec<String>> {
|
||||
if let Some(casbin_service) = &self.casbin_service {
|
||||
casbin_service
|
||||
.get_user_roles(&user_id.to_string())
|
||||
.await
|
||||
.map_err(|e| Error::new(format!("Failed to get user groups: {}", e)))
|
||||
} else {
|
||||
Err(Error::new("Casbin service not initialized"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户信息(包含组信息)
|
||||
pub async fn get_user_with_groups(&self, user_id: Uuid) -> Result<Option<UserWithGroups>> {
|
||||
let user = self.get_user_by_id(user_id).await?;
|
||||
|
||||
if let Some(user) = user {
|
||||
let groups = self.get_user_groups(&user.id).await?;
|
||||
|
||||
Ok(Some(UserWithGroups { user, groups }))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self, input: RegisterInput) -> Result<User> {
|
||||
let password_hash = self.hash_password(&input.password)?;
|
||||
let role = input.role.unwrap_or(Role::User);
|
||||
let role = Role::User;
|
||||
|
||||
// Use transaction to ensure data consistency
|
||||
let mut tx = self
|
||||
@ -113,7 +149,7 @@ impl UserService {
|
||||
|
||||
pub async fn create_user(&self, input: CreateUserInput) -> Result<User> {
|
||||
let password_hash = self.hash_password(&input.password)?;
|
||||
let role = input.role.unwrap_or(Role::User);
|
||||
let role = Role::User;
|
||||
|
||||
// Create user first
|
||||
let user = sqlx::query_as!(
|
||||
@ -160,7 +196,10 @@ impl UserService {
|
||||
.map_err(|e| Error::new(format!("Database error: {}", e)))?
|
||||
.ok_or_else(|| Error::new("Invalid credentials"))?;
|
||||
|
||||
if !self.verify_password(&input.password, &user.password_hash)? {
|
||||
if !self.verify_password(
|
||||
&input.password,
|
||||
&user.password_hash.as_ref().as_ref().unwrap(),
|
||||
)? {
|
||||
return Err(Error::new("Invalid credentials"));
|
||||
}
|
||||
|
||||
@ -197,7 +236,7 @@ impl UserService {
|
||||
sort_by: String,
|
||||
sort_order: String,
|
||||
filter: Option<String>,
|
||||
) -> Result<Vec<crate::models::user::UserInfoRow>> {
|
||||
) -> Result<Vec<User>> {
|
||||
use crate::models::user::Users;
|
||||
use sea_query::{Expr, Order, Query};
|
||||
let sort_by = Users::try_from(sort_by)?;
|
||||
@ -213,6 +252,8 @@ impl UserService {
|
||||
Users::Username,
|
||||
Users::Email,
|
||||
Users::Role,
|
||||
Users::PasswordHash,
|
||||
Users::InviteCodeId,
|
||||
Users::IsActivate,
|
||||
Users::CreatedAt,
|
||||
Users::UpdatedAt,
|
||||
@ -224,10 +265,7 @@ impl UserService {
|
||||
.offset(offset)
|
||||
.build_sqlx(PostgresQueryBuilder);
|
||||
|
||||
info!("sql: {:?}", sql);
|
||||
info!("values: {:?}", values);
|
||||
|
||||
let users = sqlx::query_as_with::<_, crate::models::user::UserInfoRow, _>(&sql, values)
|
||||
let users = sqlx::query_as_with::<_, crate::models::user::User, _>(&sql, values)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| Error::new(format!("Database error: {}", e)))?;
|
||||
@ -235,6 +273,27 @@ impl UserService {
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn get_all_users_with_groups(
|
||||
&self,
|
||||
offset: u64,
|
||||
limit: u64,
|
||||
sort_by: String,
|
||||
sort_order: String,
|
||||
filter: Option<String>,
|
||||
) -> Result<Vec<UserWithGroups>> {
|
||||
let users = self
|
||||
.get_all_users(offset, limit, sort_by, sort_order, filter)
|
||||
.await?;
|
||||
|
||||
let mut users_with_groups = Vec::new();
|
||||
|
||||
for user in users {
|
||||
let groups = self.get_user_groups(&user.id).await.unwrap_or_default();
|
||||
users_with_groups.push(UserWithGroups { user, groups });
|
||||
}
|
||||
Ok(users_with_groups)
|
||||
}
|
||||
|
||||
pub async fn initialize_admin(
|
||||
&self,
|
||||
username: String,
|
||||
@ -352,9 +411,8 @@ impl UserService {
|
||||
.await
|
||||
.map_err(|e| Error::new(format!("Database error: {}", e)))?;
|
||||
|
||||
// 并行获取用户列表
|
||||
let users = self
|
||||
.get_all_users(offset, limit, sort_by, sort_order, filter)
|
||||
let users_with_groups = self
|
||||
.get_all_users_with_groups(offset, limit, sort_by, sort_order, filter)
|
||||
.await?;
|
||||
|
||||
Ok(UserInfoRespnose {
|
||||
@ -363,7 +421,7 @@ impl UserService {
|
||||
total_inactive_users: stats.total_inactive_users.unwrap_or(0),
|
||||
total_admin_users: stats.total_admin_users.unwrap_or(0),
|
||||
total_user_users: stats.total_user_users.unwrap_or(0),
|
||||
users,
|
||||
users: users_with_groups,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user