From 1d0e96ead4ea6aca79971f67efd0776355c151a2 Mon Sep 17 00:00:00 2001 From: tsuki Date: Tue, 12 Aug 2025 21:25:17 +0800 Subject: [PATCH] add blog system --- ...3cfc5444953c0f31733ad4f1035b078d385cb.json | 22 + ...a5820278730cfe3253144cb73322fc7e9bff5.json | 2 +- ...6b3f8248aa8b2bd8878edc4f27c0d73778527.json | 14 + ...f0cbacaa6be26fb0c9ce2085b7f4961f13681.json | 21 + ...e9d4e1bac40ef1ebc022c3782794f2b60a263.json | 52 + ...f5e8ef311d65ff63311796f2bf498626bd2b7.json | 68 + ...f209459c163d9727f9393490a2782e7290ec9.json | 94 ++ ...33e646282fc1df35afa27dd2b1b91dec607cf.json | 4 +- ...25f9eb8557db25003f3b49e846c1fd685c0c5.json | 22 + ...fdb0bdae0bca20ec36b6afac183e57c0f714c.json | 22 + ...c2eea047c47beb5ee45693bdde230b64493b6.json | 68 + ...d39629adfaa3ba3767e369abb4da5fc5d82ef.json | 24 + ...514c2d5957df529fd5b3bf9acf781781853d0.json | 70 + ...4d5643874dc0c61ebfa4b1b3bbd97623c86ab.json | 34 + ...258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json | 14 + ...98abfd212f5165628caf6d3549c321bdd8584.json | 4 +- ...5c2bdd8555071031847f7cd9c2a43f9febb66.json | 22 + ...f3163b35fe21b9da0757f44141bdba7ac8efa.json | 14 + ...9412c28e2e06055f3f39bb311ca84e7f703a8.json | 70 + ...234fc0141f90bdd6e517005b04ccff7b0bc9b.json | 24 + ...5dcc39d78641efa0610879678a4cc60d10d82.json | 22 + ...1e62dc5552a3420849576b3521981b3d3537d.json | 14 + ...fcb9d9475dc902120915755c059f79151fd7b.json | 82 ++ ...40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json | 24 + ...743b97e53c11bc1f68d16ac7c4cc79388f067.json | 75 ++ ...c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json | 22 + ...0c969afbbd32595c6b94f9fed470f449a633f.json | 20 + ...94c4ff0121aedca4aea52777bc3d0085805a4.json | 20 + ...9fec43110633c7c9ae745ad9918a4fead1245.json | 20 + ...bb5377b96d7b28490db0be5d775a67a8cfcdb.json | 4 +- ...11f2dbf6b58deea19db4eb3eafa7e4e5c316a.json | 20 + ...31731a9c89f52471dd8ef353fe462fc5eb251.json | 18 + ...ea2f3d61a045f5504d9646bd051038d0f9cfb.json | 70 + ...85db3453c0eb51d154f90a89e66e8e92c8421.json | 14 + ...3c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json | 22 + ...62b0f4328f988efda30c10d3dc41d7089c35b.json | 20 + ...0db618eaee8640219493b83765eacf769b922.json | 14 + ...ad30a115851dd6664a6538fe5b85586edb0fc.json | 103 ++ ...3b6727018788d7cc361ee64a0ce9c7f6f8786.json | 75 ++ ...33b77684f4482f82fbcb81fa1d98b2968de03.json | 22 + ...0d31a7995035ff242c95b251174d56a8d0dea.json | 22 + ...a5076162d7f505ce08784769b359d8f302938.json | 4 +- ...d718b1059e04b1020ca158abd7f075552d1e2.json | 20 + ...49a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json | 75 ++ ...211367950021aa3d2a1a26539b1809521418f.json | 74 ++ ...0430d5fcc1a733a2de10453a38ae98741b89e.json | 71 + ...95b672dff25aec6fb85120366dd4341a70a3b.json | 4 +- ...afd11601b9d8266970565430627fc27e0bdf7.json | 14 + ...2bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json | 4 +- ...ff58ee972f132e8aeb66abec252338050d3fb.json | 22 + ...e8f3bf2cf50184733423c9754e8ba221285da.json | 24 + ...a9f37e72130b57cca23c8d371acc6be8721fa.json | 18 + ...02cb5eed62bc5fddf852fab1e70b76614491d.json | 52 + ...aeebf6ab8952b093780cf0d3fde20651481bf.json | 24 + ...6d446d172f8ec457cbedbcf90cd4a0be7b41f.json | 70 + ...26c7f14572099a2c26832a12cba3666218e28.json | 70 + ...903e3588535c2e17b23a280337795ec53a996.json | 20 + ...9b2a774e2510161a69ce2c0894e3968648fd7.json | 70 + ...fbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json | 82 ++ ...41e76796d61e298020c0c0d333c8797ea315d.json | 14 + ...53d982364e5e6daa4bdc13b6db119d0f189de.json | 14 + ...e3102baa587bd1c70af11e2dd7d455ab76994.json | 76 ++ ...fa83cf84e83e48490cc41c3fb64e9400ce2b4.json | 14 + ...99588744b3f05c4762af95a22cd519b82be13.json | 22 + ...3eea8bd11a2efe7aa9b578297f429f27d8cad.json | 23 + ...579beea8ade005e976a4afaa553e643aa4de1.json | 68 + ...bd9b2443bbee952788112d934c12361e486d5.json | 75 ++ ...607c3443a51a514f05ae43a4c34842b5abc7a.json | 64 + migrations/013_create_blog_system.sql | 150 +++ src/app.rs | 10 +- src/cli.rs | 219 ++++ src/graphql/mutation.rs | 144 ++- src/graphql/query.rs | 121 +- src/main.rs | 424 +++++- src/models/blog.rs | 387 ++++++ src/models/mod.rs | 2 + src/services/blog_service.rs | 1140 +++++++++++++++++ src/services/mod.rs | 1 + test_blog | 477 +++++++ 79 files changed, 5504 insertions(+), 27 deletions(-) create mode 100644 .sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json create mode 100644 .sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json create mode 100644 .sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json create mode 100644 .sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json create mode 100644 .sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json create mode 100644 .sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json create mode 100644 .sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json create mode 100644 .sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json create mode 100644 .sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json create mode 100644 .sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json create mode 100644 .sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json create mode 100644 .sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json create mode 100644 .sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json create mode 100644 .sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json create mode 100644 .sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json create mode 100644 .sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json create mode 100644 .sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json create mode 100644 .sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json create mode 100644 .sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json create mode 100644 .sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json create mode 100644 .sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json create mode 100644 .sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json create mode 100644 .sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json create mode 100644 .sqlx/query-5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f.json create mode 100644 .sqlx/query-6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4.json create mode 100644 .sqlx/query-67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245.json create mode 100644 .sqlx/query-6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a.json create mode 100644 .sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json create mode 100644 .sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json create mode 100644 .sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json create mode 100644 .sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json create mode 100644 .sqlx/query-753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b.json create mode 100644 .sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json create mode 100644 .sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json create mode 100644 .sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json create mode 100644 .sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json create mode 100644 .sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json create mode 100644 .sqlx/query-8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2.json create mode 100644 .sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json create mode 100644 .sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json create mode 100644 .sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json create mode 100644 .sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json create mode 100644 .sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json create mode 100644 .sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json create mode 100644 .sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json create mode 100644 .sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json create mode 100644 .sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json create mode 100644 .sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json create mode 100644 .sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json create mode 100644 .sqlx/query-c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996.json create mode 100644 .sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json create mode 100644 .sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json create mode 100644 .sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json create mode 100644 .sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json create mode 100644 .sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json create mode 100644 .sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json create mode 100644 .sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json create mode 100644 .sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json create mode 100644 .sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json create mode 100644 .sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json create mode 100644 .sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json create mode 100644 migrations/013_create_blog_system.sql create mode 100644 src/models/blog.rs create mode 100644 src/services/blog_service.rs create mode 100644 test_blog diff --git a/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json b/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json new file mode 100644 index 0000000..2df5961 --- /dev/null +++ b/.sqlx/query-006684071c99cb1c0801395a7723cfc5444953c0f31733ad4f1035b078d385cb.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json b/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json index c94f796..3274547 100644 --- a/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json +++ b/.sqlx/query-035b45ec1dafdf984b6e4a0e935a5820278730cfe3253144cb73322fc7e9bff5.json @@ -36,7 +36,7 @@ { "ordinal": 6, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" } ], "parameters": { diff --git a/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json b/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json new file mode 100644 index 0000000..fdf8aba --- /dev/null +++ b/.sqlx/query-063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM data_points WHERE chart_block_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "063b7d3782eb878facd48a0d24c6b3f8248aa8b2bd8878edc4f27c0d73778527" +} diff --git a/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json b/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json new file mode 100644 index 0000000..8adf917 --- /dev/null +++ b/.sqlx/query-08d8ede4405c75d35650b1d96d6f0cbacaa6be26fb0c9ce2085b7f4961f13681.json @@ -0,0 +1,21 @@ +{ + "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" +} diff --git a/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json b/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json new file mode 100644 index 0000000..0c3e9b6 --- /dev/null +++ b/.sqlx/query-0dfd961e9f4524bde4bc151d7a7e9d4e1bac40ef1ebc022c3782794f2b60a263.json @@ -0,0 +1,52 @@ +{ + "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" +} diff --git a/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json b/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json new file mode 100644 index 0000000..c2d117b --- /dev/null +++ b/.sqlx/query-0e04293e919fb78f8b908a495f6f5e8ef311d65ff63311796f2bf498626bd2b7.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json b/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json new file mode 100644 index 0000000..7cb7893 --- /dev/null +++ b/.sqlx/query-12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9.json @@ -0,0 +1,94 @@ +{ + "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\", updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 12, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + true, + true, + true, + false, + false, + false + ] + }, + "hash": "12ac5e85271d509743c9e4e199bf209459c163d9727f9393490a2782e7290ec9" +} diff --git a/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json b/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json index cd05a0b..23eb924 100644 --- a/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json +++ b/.sqlx/query-19c23596ea8098fdf9a13f4a4f033e646282fc1df35afa27dd2b1b91dec607cf.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json b/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json new file mode 100644 index 0000000..c5bf89e --- /dev/null +++ b/.sqlx/query-1a070734e3029a0bafa3b3e175925f9eb8557db25003f3b49e846c1fd685c0c5.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json b/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json new file mode 100644 index 0000000..3268697 --- /dev/null +++ b/.sqlx/query-1f4479ead07e8c7b3188251d166fdb0bdae0bca20ec36b6afac183e57c0f714c.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json b/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json new file mode 100644 index 0000000..502aac8 --- /dev/null +++ b/.sqlx/query-20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6.json @@ -0,0 +1,68 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Int4", + "Bool", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "20146adf2fda79bf8231af4f79dc2eea047c47beb5ee45693bdde230b64493b6" +} diff --git a/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json b/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json new file mode 100644 index 0000000..bbbe30f --- /dev/null +++ b/.sqlx/query-250839c4ff5724972b78b1eaec6d39629adfaa3ba3767e369abb4da5fc5d82ef.json @@ -0,0 +1,24 @@ +{ + "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" +} diff --git a/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json b/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json new file mode 100644 index 0000000..987f8ac --- /dev/null +++ b/.sqlx/query-285bbb0fac6996ba3c061dd52e3514c2d5957df529fd5b3bf9acf781781853d0.json @@ -0,0 +1,70 @@ +{ + "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" +} diff --git a/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json b/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json new file mode 100644 index 0000000..ea8ecc8 --- /dev/null +++ b/.sqlx/query-2d32cb71ebda39278cfe19cf6f84d5643874dc0c61ebfa4b1b3bbd97623c86ab.json @@ -0,0 +1,34 @@ +{ + "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" +} diff --git a/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json b/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json new file mode 100644 index 0000000..76e396b --- /dev/null +++ b/.sqlx/query-32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM hero_blocks WHERE page_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "32079198d949123912be2fd9a5f258a29c443fdda04f5dfc06c2d7a0bcb5e31d" +} diff --git a/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json b/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json index cf1c5c1..d671e85 100644 --- a/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json +++ b/.sqlx/query-337689745b15c94365b9ab55c4298abfd212f5165628caf6d3549c321bdd8584.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json b/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json new file mode 100644 index 0000000..5eaa4a3 --- /dev/null +++ b/.sqlx/query-34db85e1513819a46067e2412145c2bdd8555071031847f7cd9c2a43f9febb66.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json b/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json new file mode 100644 index 0000000..e7586fd --- /dev/null +++ b/.sqlx/query-36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM settings_blocks WHERE page_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "36e750deedcdd11bad52c216695f3163b35fe21b9da0757f44141bdba7ac8efa" +} diff --git a/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json b/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json new file mode 100644 index 0000000..d797edb --- /dev/null +++ b/.sqlx/query-37f549d3f13dedc4d10e31cf4979412c28e2e06055f3f39bb311ca84e7f703a8.json @@ -0,0 +1,70 @@ +{ + "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" +} diff --git a/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json b/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json new file mode 100644 index 0000000..1bcc331 --- /dev/null +++ b/.sqlx/query-37f8e6d81450fd2a18a8b201cfa234fc0141f90bdd6e517005b04ccff7b0bc9b.json @@ -0,0 +1,24 @@ +{ + "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" +} diff --git a/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json b/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json new file mode 100644 index 0000000..7ba81cd --- /dev/null +++ b/.sqlx/query-3fdd78820ae558e250ffe01931f5dcc39d78641efa0610879678a4cc60d10d82.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json b/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json new file mode 100644 index 0000000..3e84f4a --- /dev/null +++ b/.sqlx/query-45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM table_blocks WHERE page_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "45d9c3d0fc785b8e0ca6056de391e62dc5552a3420849576b3521981b3d3537d" +} diff --git a/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json b/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json new file mode 100644 index 0000000..24bd2b8 --- /dev/null +++ b/.sqlx/query-4721f4799049974626de030a22ffcb9d9475dc902120915755c059f79151fd7b.json @@ -0,0 +1,82 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json b/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json new file mode 100644 index 0000000..6b16447 --- /dev/null +++ b/.sqlx/query-4d880f3063c6a80fb091867b94e40ebc6aef1e1053b8992b23c92b5e4f63c8a3.json @@ -0,0 +1,24 @@ +{ + "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" +} diff --git a/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json b/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json new file mode 100644 index 0000000..94edbe6 --- /dev/null +++ b/.sqlx/query-4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067.json @@ -0,0 +1,75 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int4", + "Varchar", + "Varchar", + "Jsonb", + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "4dec15acfaf647eaee056655cb3743b97e53c11bc1f68d16ac7c4cc79388f067" +} diff --git a/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json b/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json new file mode 100644 index 0000000..afcda0d --- /dev/null +++ b/.sqlx/query-5c667496c3cb12aa0fb4e498de6c1a5bc6424e078b0d19c1bc1af4b70e0d5abe.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f.json b/.sqlx/query-5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f.json new file mode 100644 index 0000000..f1879c7 --- /dev/null +++ b/.sqlx/query-5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COALESCE(SUM(view_count), 0) FROM blogs WHERE is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "coalesce", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "5f747eb2f4b37872b7dd9cc83fc0c969afbbd32595c6b94f9fed470f449a633f" +} diff --git a/.sqlx/query-6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4.json b/.sqlx/query-6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4.json new file mode 100644 index 0000000..0d5cb51 --- /dev/null +++ b/.sqlx/query-6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blogs WHERE status = 'draft' AND is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "6224838e35f5aaa67c3f9f94de194c4ff0121aedca4aea52777bc3d0085805a4" +} diff --git a/.sqlx/query-67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245.json b/.sqlx/query-67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245.json new file mode 100644 index 0000000..e8bea93 --- /dev/null +++ b/.sqlx/query-67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blog_categories WHERE is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "67f4ecc3acdf4abfa9fc45c0d889fec43110633c7c9ae745ad9918a4fead1245" +} diff --git a/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json b/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json index 2cefba2..bd69228 100644 --- a/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json +++ b/.sqlx/query-6bab42214f2c28a3985afa5ce4bbb5377b96d7b28490db0be5d775a67a8cfcdb.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a.json b/.sqlx/query-6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a.json new file mode 100644 index 0000000..49c9cbf --- /dev/null +++ b/.sqlx/query-6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blog_tags WHERE is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "6c7cb940fca35c491af8b4b07c111f2dbf6b58deea19db4eb3eafa7e4e5c316a" +} diff --git a/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json b/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json new file mode 100644 index 0000000..e73cc03 --- /dev/null +++ b/.sqlx/query-6cbfa10af054707a593004f379031731a9c89f52471dd8ef353fe462fc5eb251.json @@ -0,0 +1,18 @@ +{ + "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" +} diff --git a/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json b/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json new file mode 100644 index 0000000..ed5e47f --- /dev/null +++ b/.sqlx/query-7049523511ae5c39c433eedecd8ea2f3d61a045f5504d9646bd051038d0f9cfb.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json b/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json new file mode 100644 index 0000000..5489283 --- /dev/null +++ b/.sqlx/query-74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM pages WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "74b30e4a74f27fa5d0e7f5a758185db3453c0eb51d154f90a89e66e8e92c8421" +} diff --git a/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json b/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json new file mode 100644 index 0000000..495bca7 --- /dev/null +++ b/.sqlx/query-74bd3010cb42f1a124df3ecfbb93c3feafb25eaf1730343ba0ba4e7c9fd7cf1b.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b.json b/.sqlx/query-753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b.json new file mode 100644 index 0000000..b04b240 --- /dev/null +++ b/.sqlx/query-753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blogs WHERE is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "753787e580c2fc8c48bafc58b8d62b0f4328f988efda30c10d3dc41d7089c35b" +} diff --git a/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json b/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json new file mode 100644 index 0000000..de32bb8 --- /dev/null +++ b/.sqlx/query-76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM chart_blocks WHERE page_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "76d3196348484270fd83699229c0db618eaee8640219493b83765eacf769b922" +} diff --git a/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json b/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json new file mode 100644 index 0000000..707ec0e --- /dev/null +++ b/.sqlx/query-76ebd5c5e6d6234192b323dcbc1ad30a115851dd6664a6538fe5b85586edb0fc.json @@ -0,0 +1,103 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 12, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json b/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json new file mode 100644 index 0000000..6e2a625 --- /dev/null +++ b/.sqlx/query-79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786.json @@ -0,0 +1,75 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Int4", + "Bool", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false + ] + }, + "hash": "79c26fa0a70d890f43063e151653b6727018788d7cc361ee64a0ce9c7f6f8786" +} diff --git a/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json b/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json new file mode 100644 index 0000000..60f9127 --- /dev/null +++ b/.sqlx/query-7ee383b9cd74d3a94c543878b8933b77684f4482f82fbcb81fa1d98b2968de03.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json b/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json new file mode 100644 index 0000000..4db9dcc --- /dev/null +++ b/.sqlx/query-7f6785c40d5a768b4b67685d6f90d31a7995035ff242c95b251174d56a8d0dea.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json b/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json index 2cc64d5..6123ba5 100644 --- a/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json +++ b/.sqlx/query-82c99538d54085bc56abe4fb049a5076162d7f505ce08784769b359d8f302938.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2.json b/.sqlx/query-8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2.json new file mode 100644 index 0000000..6ee7c35 --- /dev/null +++ b/.sqlx/query-8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blogs WHERE status = 'published' AND is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "8a3cb562905ba7ebac3000a29f9d718b1059e04b1020ca158abd7f075552d1e2" +} diff --git a/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json b/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json new file mode 100644 index 0000000..4b538f9 --- /dev/null +++ b/.sqlx/query-8ad20bc169ba756b347954c4c3849a2ac1ecbd40f8695e3c9021a4f84eaf54ad.json @@ -0,0 +1,75 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json b/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json new file mode 100644 index 0000000..cc66807 --- /dev/null +++ b/.sqlx/query-8dbecde6ae9d3e259a0239a2848211367950021aa3d2a1a26539b1809521418f.json @@ -0,0 +1,74 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json b/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json new file mode 100644 index 0000000..307e6ec --- /dev/null +++ b/.sqlx/query-8fcc976536dcff8c9120ef11a020430d5fcc1a733a2de10453a38ae98741b89e.json @@ -0,0 +1,71 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json b/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json index 8b1a298..9348f49 100644 --- a/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json +++ b/.sqlx/query-8fe2f86e9388d6ed22b1b3ac4c595b672dff25aec6fb85120366dd4341a70a3b.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json b/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json new file mode 100644 index 0000000..eb42654 --- /dev/null +++ b/.sqlx/query-911ba2c819ab358e455aa87627fafd11601b9d8266970565430627fc27e0bdf7.json @@ -0,0 +1,14 @@ +{ + "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" +} diff --git a/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json b/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json index ccc10d8..55a7ab1 100644 --- a/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json +++ b/.sqlx/query-93734fffdd102bda19488e703012bdad22bf3675a9b2dbeaf7943c9d0341eaf2.json @@ -51,12 +51,12 @@ { "ordinal": 9, "name": "created_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 10, "name": "updated_at: DateTime", - "type_info": "Timestamp" + "type_info": "Timestamptz" }, { "ordinal": 11, diff --git a/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json b/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json new file mode 100644 index 0000000..82543c1 --- /dev/null +++ b/.sqlx/query-94a29b4c81cbad35681b776c22fff58ee972f132e8aeb66abec252338050d3fb.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json b/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json new file mode 100644 index 0000000..9ca93a7 --- /dev/null +++ b/.sqlx/query-94d1a5e4289defa7afd41e6640be8f3bf2cf50184733423c9754e8ba221285da.json @@ -0,0 +1,24 @@ +{ + "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" +} diff --git a/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json b/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json new file mode 100644 index 0000000..475e25b --- /dev/null +++ b/.sqlx/query-97a1320fd98948fcba950bb04d7a9f37e72130b57cca23c8d371acc6be8721fa.json @@ -0,0 +1,18 @@ +{ + "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" +} diff --git a/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json b/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json new file mode 100644 index 0000000..1c25900 --- /dev/null +++ b/.sqlx/query-a8005db05313b629d7e3ed5d98a02cb5eed62bc5fddf852fab1e70b76614491d.json @@ -0,0 +1,52 @@ +{ + "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" +} diff --git a/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json b/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json new file mode 100644 index 0000000..de1d825 --- /dev/null +++ b/.sqlx/query-a91efb27039b215feafafe0ea35aeebf6ab8952b093780cf0d3fde20651481bf.json @@ -0,0 +1,24 @@ +{ + "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" +} diff --git a/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json b/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json new file mode 100644 index 0000000..1e96cd1 --- /dev/null +++ b/.sqlx/query-ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "ab667fa7f6f6a8bef7837b7c0ac6d446d172f8ec457cbedbcf90cd4a0be7b41f" +} diff --git a/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json b/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json new file mode 100644 index 0000000..b053332 --- /dev/null +++ b/.sqlx/query-bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false + ] + }, + "hash": "bf0d70ab0f89920b684f0f3e7eb26c7f14572099a2c26832a12cba3666218e28" +} diff --git a/.sqlx/query-c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996.json b/.sqlx/query-c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996.json new file mode 100644 index 0000000..86c504f --- /dev/null +++ b/.sqlx/query-c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM blogs WHERE status = 'archived' AND is_active = true", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null + ] + }, + "hash": "c2370c206aeebe9102fb103dece903e3588535c2e17b23a280337795ec53a996" +} diff --git a/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json b/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json new file mode 100644 index 0000000..7ecbaef --- /dev/null +++ b/.sqlx/query-c60bc0b511ce32dfe64b4e572a89b2a774e2510161a69ce2c0894e3968648fd7.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, title, slug, description, is_active, \n created_at as \"created_at: DateTime\", \n updated_at as \"updated_at: DateTime\", \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", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json b/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json new file mode 100644 index 0000000..0fde01d --- /dev/null +++ b/.sqlx/query-c65d91fb0d8f721d2915b399bcbfbb3b2e491c2e1d2ef29f4a27e0372e1e2640.json @@ -0,0 +1,82 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated_at: DateTime", + "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" +} diff --git a/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json b/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json new file mode 100644 index 0000000..b7d8700 --- /dev/null +++ b/.sqlx/query-cecf12cc6887ce7ec52c50b1b0041e76796d61e298020c0c0d333c8797ea315d.json @@ -0,0 +1,14 @@ +{ + "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" +} diff --git a/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json b/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json new file mode 100644 index 0000000..f8302cd --- /dev/null +++ b/.sqlx/query-d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM text_blocks WHERE page_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "d5f900c12fb2c8def741b8cef4053d982364e5e6daa4bdc13b6db119d0f189de" +} diff --git a/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json b/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json new file mode 100644 index 0000000..32810bf --- /dev/null +++ b/.sqlx/query-ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994.json @@ -0,0 +1,76 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "ddca5b33d5e464ce263c6fc38b6e3102baa587bd1c70af11e2dd7d455ab76994" +} diff --git a/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json b/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json new file mode 100644 index 0000000..af6361e --- /dev/null +++ b/.sqlx/query-e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM table_columns WHERE table_block_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "e73a002aba1d824ed2f0ee5314cfa83cf84e83e48490cc41c3fb64e9400ce2b4" +} diff --git a/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json b/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json new file mode 100644 index 0000000..2047a5f --- /dev/null +++ b/.sqlx/query-ed0c216842515dd047b07f4cddf99588744b3f05c4762af95a22cd519b82be13.json @@ -0,0 +1,22 @@ +{ + "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" +} diff --git a/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json b/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json new file mode 100644 index 0000000..6ce954a --- /dev/null +++ b/.sqlx/query-fd4b2f69a310a48efe720f4cf833eea8bd11a2efe7aa9b578297f429f27d8cad.json @@ -0,0 +1,23 @@ +{ + "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" +} diff --git a/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json b/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json new file mode 100644 index 0000000..6372dfb --- /dev/null +++ b/.sqlx/query-fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1.json @@ -0,0 +1,68 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Int4", + "Varchar", + "Text", + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "fe4c7e3637409a4b8d7c3385ccb579beea8ade005e976a4afaa553e643aa4de1" +} diff --git a/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json b/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json new file mode 100644 index 0000000..0cd05e9 --- /dev/null +++ b/.sqlx/query-fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5.json @@ -0,0 +1,75 @@ +{ + "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\", \n updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Jsonb", + "Int4", + "Bool", + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + false + ] + }, + "hash": "fe7162f56bbaec5f10c2095343abd9b2443bbee952788112d934c12361e486d5" +} diff --git a/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json b/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json new file mode 100644 index 0000000..12eee4f --- /dev/null +++ b/.sqlx/query-fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a.json @@ -0,0 +1,64 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, page_id, block_order, title, markdown, is_active, created_at as \"created_at: DateTime\", updated_at as \"updated_at: DateTime\"\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", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "updated_at: DateTime", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "fec82028aca77848dd54023e9a6607c3443a51a514f05ae43a4c34842b5abc7a" +} diff --git a/migrations/013_create_blog_system.sql b/migrations/013_create_blog_system.sql new file mode 100644 index 0000000..1b03a62 --- /dev/null +++ b/migrations/013_create_blog_system.sql @@ -0,0 +1,150 @@ +-- Blog 系统数据库迁移 +-- 创建博客分类表 +CREATE TABLE IF NOT EXISTS blog_categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL UNIQUE, + slug VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + color VARCHAR(7), -- 十六进制颜色代码,如 #FF5733 + icon VARCHAR(100), -- 图标名称或路径 + is_active BOOLEAN DEFAULT true, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- 创建博客标签表 +CREATE TABLE IF NOT EXISTS blog_tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL UNIQUE, + slug VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + color VARCHAR(7), -- 十六进制颜色代码 + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- 创建博客文章表 +CREATE TABLE IF NOT EXISTS blogs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(500) NOT NULL, + slug VARCHAR(500) NOT NULL UNIQUE, + excerpt TEXT, -- 摘要 + content JSONB NOT NULL, -- JSON格式的博客内容 + category_id UUID REFERENCES blog_categories(id), + status VARCHAR(20) DEFAULT 'draft', -- draft, published, archived + featured_image VARCHAR(500), -- 特色图片URL + meta_title VARCHAR(255), -- SEO标题 + meta_description TEXT, -- SEO描述 + published_at TIMESTAMPTZ, -- 发布时间 + view_count INTEGER DEFAULT 0, -- 浏览次数 + is_featured BOOLEAN DEFAULT false, -- 是否推荐 + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID REFERENCES users(id), + updated_by UUID REFERENCES users(id) +); + +-- 创建博客标签关联表(多对多关系) +CREATE TABLE IF NOT EXISTS blog_tag_relations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + blog_id UUID NOT NULL REFERENCES blogs(id) ON DELETE CASCADE, + tag_id UUID NOT NULL REFERENCES blog_tags(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(blog_id, tag_id) +); + +-- 创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_blog_categories_slug ON blog_categories(slug); +CREATE INDEX IF NOT EXISTS idx_blog_categories_active ON blog_categories(is_active); +CREATE INDEX IF NOT EXISTS idx_blog_categories_sort ON blog_categories(sort_order); + +CREATE INDEX IF NOT EXISTS idx_blog_tags_slug ON blog_tags(slug); +CREATE INDEX IF NOT EXISTS idx_blog_tags_active ON blog_tags(is_active); + +CREATE INDEX IF NOT EXISTS idx_blogs_slug ON blogs(slug); +CREATE INDEX IF NOT EXISTS idx_blogs_status ON blogs(status); +CREATE INDEX IF NOT EXISTS idx_blogs_category ON blogs(category_id); +CREATE INDEX IF NOT EXISTS idx_blogs_published_at ON blogs(published_at); +CREATE INDEX IF NOT EXISTS idx_blogs_featured ON blogs(is_featured); +CREATE INDEX IF NOT EXISTS idx_blogs_active ON blogs(is_active); +CREATE INDEX IF NOT EXISTS idx_blogs_created_at ON blogs(created_at); +CREATE INDEX IF NOT EXISTS idx_blogs_view_count ON blogs(view_count); + +CREATE INDEX IF NOT EXISTS idx_blog_tag_relations_blog ON blog_tag_relations(blog_id); +CREATE INDEX IF NOT EXISTS idx_blog_tag_relations_tag ON blog_tag_relations(tag_id); + +-- 创建全文搜索索引 +CREATE INDEX IF NOT EXISTS idx_blogs_content_gin ON blogs USING GIN (content); +-- CREATE INDEX IF NOT EXISTS idx_blogs_title_gin ON blogs USING GIN (to_tsvector('chinese', title)); +-- CREATE INDEX IF NOT EXISTS idx_blogs_excerpt_gin ON blogs USING GIN (to_tsvector('chinese', excerpt)); + +-- 创建触发器更新 updated_at +CREATE TRIGGER update_blog_categories_updated_at BEFORE UPDATE +ON blog_categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_blog_tags_updated_at BEFORE UPDATE +ON blog_tags FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_blogs_updated_at BEFORE UPDATE +ON blogs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 插入默认分类 +INSERT INTO blog_categories (name, slug, description, color, icon, sort_order, created_at, updated_at) VALUES +('Announcement', 'announcement', 'Announcement', '#3B82F6', 'code', 1, NOW(), NOW()), +('Product Update', 'product-update', 'Product Update', '#10B981', 'bell', 2, NOW(), NOW()), +('Usage Guide', 'usage-guide', 'Usage Guide', '#F59E0B', 'book-open', 3, NOW(), NOW()), +('Industry Trend', 'industry-trend', 'Industry Trend', '#8B5CF6', 'trending-up', 4, NOW(), NOW()), +('Team Story', 'team-story', 'Team Story', '#EF4444', 'users', 5, NOW(), NOW()); + +-- 插入默认标签 +INSERT INTO blog_tags (name, slug, description, color, created_at, updated_at) VALUES +('Rust', 'rust', 'Rust编程语言相关', '#CE422B', NOW(), NOW()); + +-- 为Casbin添加Blog相关权限 +INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) VALUES +-- 管理员Blog权限 +('p', 'admin', 'blogs', 'read', '', '', ''), +('p', 'admin', 'blogs', 'write', '', '', ''), +('p', 'admin', 'blogs', 'delete', '', '', ''), +('p', 'admin', 'blog_categories', 'read', '', '', ''), +('p', 'admin', 'blog_categories', 'write', '', '', ''), +('p', 'admin', 'blog_categories', 'delete', '', '', ''), +('p', 'admin', 'blog_tags', 'read', '', '', ''), +('p', 'admin', 'blog_tags', 'write', '', '', ''), +('p', 'admin', 'blog_tags', 'delete', '', '', ''), + +-- 用户Blog权限(只读) +('p', 'user', 'blogs', 'read', '', '', ''), +('p', 'user', 'blog_categories', 'read', '', '', ''), +('p', 'user', 'blog_tags', 'read', '', '', ''); + +-- 添加Blog相关权限到permissions表 +INSERT INTO permissions (name, description, resource, action, is_active, created_at, updated_at) VALUES +-- Blog管理权限 +('blogs_read', '读取博客文章', 'blogs', 'read', true, NOW(), NOW()), +('blogs_write', '创建/修改博客文章', 'blogs', 'write', true, NOW(), NOW()), +('blogs_delete', '删除博客文章', 'blogs', 'delete', true, NOW(), NOW()), + +-- 博客分类管理权限 +('blog_categories_read', '读取博客分类', 'blog_categories', 'read', true, NOW(), NOW()), +('blog_categories_write', '创建/修改博客分类', 'blog_categories', 'write', true, NOW(), NOW()), +('blog_categories_delete', '删除博客分类', 'blog_categories', 'delete', true, NOW(), NOW()), + +-- 博客标签管理权限 +('blog_tags_read', '读取博客标签', 'blog_tags', 'read', true, NOW(), NOW()), +('blog_tags_write', '创建/修改博客标签', 'blog_tags', 'write', true, NOW(), NOW()), +('blog_tags_delete', '删除博客标签', 'blog_tags', 'delete', true, NOW(), NOW()); + +-- 为角色分配Blog权限 +INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at) +SELECT 'admin', id, NULL, NOW() FROM permissions WHERE resource IN ('blogs', 'blog_categories', 'blog_tags'); + +INSERT INTO role_permissions (role_name, permission_id, granted_by, granted_at) +SELECT 'user', id, NULL, NOW() FROM permissions WHERE resource IN ('blogs', 'blog_categories', 'blog_tags') AND action = 'read'; \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index ae2c7f6..afb35d1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -24,10 +24,10 @@ use crate::{ config::Config, graphql::{subscription::StatusUpdate, MutationRoot, QueryRoot, SubscriptionRoot}, services::{ - 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, + invite_code_service::InviteCodeService, mosaic_service::MosaicService, + page_block_service::PageBlockService, settings_service::SettingsService, + system_config_service::SystemConfigService, user_service::UserService, }, }; @@ -54,6 +54,7 @@ pub async fn create_router( 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 @@ -67,6 +68,7 @@ pub async fn create_router( .data(mosaic_service) .data(settings_service) .data(page_block_service) + .data(blog_service) .data(casbin_service) .data(config.clone()) .data(status_sender.clone()) diff --git a/src/cli.rs b/src/cli.rs index 9d2f8ed..c43a739 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,8 @@ pub enum Commands { Migrate(MigrateArgs), /// 权限管理 Permissions(PermissionsArgs), + /// 博客管理 + Blog(BlogArgs), /// 显示版本信息 Version, /// 显示配置信息 @@ -180,6 +182,223 @@ pub struct CheckPermissionArgs { pub action: String, } +/// 博客管理相关命令 +#[derive(Subcommand)] +pub enum BlogCommands { + /// 创建博客文章 + Create(CreateBlogArgs), + /// 列出博客文章 + List(ListBlogArgs), + /// 查看博客文章详情 + Show(ShowBlogArgs), + /// 更新博客文章 + Update(UpdateBlogArgs), + /// 删除博客文章 + Delete(DeleteBlogArgs), + /// 创建博客分类 + CreateCategory(CreateCategoryArgs), + /// 列出博客分类 + ListCategories, + /// 创建博客标签 + CreateTag(CreateTagArgs), + /// 列出博客标签 + ListTags, + /// 博客统计信息 + Stats, +} + +/// 博客管理参数 +#[derive(Args)] +pub struct BlogArgs { + #[command(subcommand)] + pub command: BlogCommands, +} + +/// 创建博客文章参数 +#[derive(Args)] +pub struct CreateBlogArgs { + /// 文章标题 + #[arg(short, long)] + pub title: String, + + /// 文章slug(URL友好标识符) + #[arg(short, long)] + pub slug: String, + + /// 文章摘要 + #[arg(short = 'e', long)] + pub excerpt: Option, + + /// 文章内容(JSON格式) + #[arg(short, long)] + pub content: String, + + /// 分类ID + #[arg(short = 'C', long)] + pub category_id: Option, + + /// 文章状态(draft, published, archived) + #[arg(short = 'S', long, default_value = "draft")] + pub status: String, + + /// 是否为推荐文章 + #[arg(short, long)] + pub featured: bool, + + /// 标签ID列表(逗号分隔) + #[arg(short = 'T', long)] + pub tags: Option, + + /// 作者用户ID + #[arg(short = 'u', long)] + pub user_id: String, +} + +/// 列出博客文章参数 +#[derive(Args)] +pub struct ListBlogArgs { + /// 页码 + #[arg(short, long, default_value = "1")] + pub page: i32, + + /// 每页数量 + #[arg(short = 'l', long, default_value = "10")] + pub limit: i32, + + /// 按状态过滤 + #[arg(short, long)] + pub status: Option, + + /// 按分类过滤 + #[arg(short, long)] + pub category: Option, + + /// 搜索关键词 + #[arg(short = 'q', long)] + pub search: Option, + + /// 只显示推荐文章 + #[arg(short, long)] + pub featured: bool, +} + +/// 查看博客文章参数 +#[derive(Args)] +pub struct ShowBlogArgs { + /// 博客文章ID或slug + pub id_or_slug: String, + + /// 显示详细信息(包括分类和标签) + #[arg(short, long)] + pub detail: bool, +} + +/// 更新博客文章参数 +#[derive(Args)] +pub struct UpdateBlogArgs { + /// 博客文章ID + pub id: String, + + /// 文章标题 + #[arg(short, long)] + pub title: Option, + + /// 文章slug + #[arg(short, long)] + pub slug: Option, + + /// 文章摘要 + #[arg(short = 'e', long)] + pub excerpt: Option, + + /// 文章内容(JSON格式) + #[arg(short, long)] + pub content: Option, + + /// 分类ID + #[arg(short = 'C', long)] + pub category_id: Option, + + /// 文章状态 + #[arg(short = 'S', long)] + pub status: Option, + + /// 是否为推荐文章 + #[arg(short, long)] + pub featured: Option, + + /// 标签ID列表(逗号分隔) + #[arg(short = 'T', long)] + pub tags: Option, + + /// 更新者用户ID + #[arg(short = 'u', long)] + pub user_id: String, +} + +/// 删除博客文章参数 +#[derive(Args)] +pub struct DeleteBlogArgs { + /// 博客文章ID + pub id: String, +} + +/// 创建博客分类参数 +#[derive(Args)] +pub struct CreateCategoryArgs { + /// 分类名称 + #[arg(short, long)] + pub name: String, + + /// 分类slug + #[arg(short, long)] + pub slug: String, + + /// 分类描述 + #[arg(short, long)] + pub description: Option, + + /// 分类颜色 + #[arg(short, long)] + pub color: Option, + + /// 分类图标 + #[arg(short, long)] + pub icon: Option, + + /// 排序顺序 + #[arg(short = 'o', long, default_value = "0")] + pub sort_order: i32, + + /// 创建者用户ID + #[arg(short = 'u', long)] + pub user_id: String, +} + +/// 创建博客标签参数 +#[derive(Args)] +pub struct CreateTagArgs { + /// 标签名称 + #[arg(short, long)] + pub name: String, + + /// 标签slug + #[arg(short, long)] + pub slug: String, + + /// 标签描述 + #[arg(short, long)] + pub description: Option, + + /// 标签颜色 + #[arg(short, long)] + pub color: Option, + + /// 创建者用户ID + #[arg(short = 'u', long)] + pub user_id: String, +} + impl Default for Commands { fn default() -> Self { Commands::Serve(ServeArgs { diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs index 353db39..bcf60e8 100644 --- a/src/graphql/mutation.rs +++ b/src/graphql/mutation.rs @@ -1,6 +1,5 @@ use crate::auth::get_auth_user; -use crate::graphql::guards::RequireRole; -use crate::graphql::guards::RequireWritePermission; +use crate::graphql::guards::{RequireReadPermission, RequireRole, RequireWritePermission}; use crate::graphql::types::ConfigUpdateResultType; use crate::graphql::types::UpdateDocsSupportInput; use crate::graphql::types::UpdateModalAnnouncementInput; @@ -8,15 +7,24 @@ use crate::graphql::types::UpdateNoticeConfigInput; use crate::graphql::types::UpdateOpsConfigInput; use crate::graphql::types::UpdateSiteConfigInput; use crate::graphql::types::*; +use crate::models::blog::{ + CreateBlogCategoryInput, CreateBlogInput, CreateBlogTagInput, UpdateBlogCategoryInput, + UpdateBlogInput, UpdateBlogTagInput, +}; use crate::models::page_block::*; use crate::models::settings::{CreateSetting, UpdateSetting}; use crate::models::user::Role; use crate::models::user::User; +use crate::models::Blog; +use crate::models::BlogCategory; +use crate::models::BlogTag; +use crate::services::blog_service::BlogService; use crate::services::casbin_service::CasbinService; use crate::services::invite_code_service::InviteCodeService; use crate::services::page_block_service::PageBlockService; use crate::services::settings_service::SettingsService; use crate::services::user_service::UserService; +use async_graphql::Error as GraphQLError; use async_graphql::{Context, Object, Result}; use uuid::Uuid; @@ -1170,4 +1178,136 @@ impl MutationRoot { .collect(), }) } + + // ==================== Blog 相关变更 ==================== + + /// 创建博客文章 + #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] + async fn create_blog(&self, ctx: &Context<'_>, input: CreateBlogInput) -> Result { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_blog(input, auth_user.id) + .await + .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 { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_blog(id, input, auth_user.id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 删除博客文章 + #[graphql(guard = "RequireWritePermission::new(\"blogs\")")] + async fn delete_blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + 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 { + let blog_service = ctx.data::()?; + 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 { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_category(input, auth_user.id) + .await + .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 { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_category(id, input, auth_user.id) + .await + .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 { + let blog_service = ctx.data::()?; + 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 { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .create_tag(input, auth_user.id) + .await + .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 { + let auth_user = get_auth_user(ctx).await?; + let blog_service = ctx.data::()?; + blog_service + .update_tag(id, input, auth_user.id) + .await + .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 { + let blog_service = ctx.data::()?; + blog_service + .delete_tag(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } } diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 0c6b8e3..a9d7106 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -1,18 +1,23 @@ use crate::auth::get_auth_user; use crate::graphql::guards::{ - RequireDeletePermission, RequireMultiplePermissions, RequirePermission, RequireReadPermission, - RequireRole, RequireWritePermission, + RequireMultiplePermissions, RequirePermission, RequireReadPermission, }; use crate::graphql::types::*; -use crate::graphql::types::*; +use crate::models::blog::{ + Blog, BlogCategory, BlogCategoryFilterInput, BlogDetail, BlogFilterInput, BlogSortInput, + BlogStats, BlogTag, BlogTagFilterInput, +}; use crate::models::invite_code::InviteCode; -use crate::models::settings::{SettingFilter, SettingHistory}; -use crate::models::user::{Role, User, UserInfoRow}; +use crate::models::page_block::{PaginatedResult, PaginationInput}; +use crate::models::settings::SettingFilter; +use crate::models::user::{User, UserInfoRow}; +use crate::services::blog_service::BlogService; use crate::services::casbin_service::CasbinService; use crate::services::invite_code_service::InviteCodeService; use crate::services::page_block_service::PageBlockService; use crate::services::settings_service::SettingsService; use crate::services::user_service::UserService; +use async_graphql::Error as GraphQLError; use async_graphql::{Context, Object, Result}; use tracing::info; use uuid::Uuid; @@ -1238,4 +1243,110 @@ impl QueryRoot { warnings, }) } + + // ==================== Blog 相关查询 ==================== + + /// 获取博客文章列表 + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn blogs( + &self, + ctx: &Context<'_>, + filter: Option, + sort: Option, + pagination: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + blog_service + .get_blogs(filter, sort, pagination) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 根据ID获取博客文章 + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_blog_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 根据slug获取博客文章 + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_blog_by_slug(&slug) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 获取博客文章详情(包含分类和标签) + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_blog_detail(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 获取博客统计信息 + #[graphql(guard = "RequireReadPermission::new(\"blogs\")")] + async fn blog_stats(&self, ctx: &Context<'_>) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_blog_stats() + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 获取博客分类列表 + #[graphql(guard = "RequireReadPermission::new(\"blog_categories\")")] + async fn blog_categories( + &self, + ctx: &Context<'_>, + filter: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + blog_service + .get_categories(filter) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 根据ID获取博客分类 + #[graphql(guard = "RequireReadPermission::new(\"blog_categories\")")] + async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_category_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 获取博客标签列表 + #[graphql(guard = "RequireReadPermission::new(\"blog_tags\")")] + async fn blog_tags( + &self, + ctx: &Context<'_>, + filter: Option, + ) -> Result> { + let blog_service = ctx.data::()?; + blog_service + .get_tags(filter) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } + + /// 根据ID获取博客标签 + #[graphql(guard = "RequireReadPermission::new(\"blog_tags\")")] + async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { + let blog_service = ctx.data::()?; + blog_service + .get_tag_by_id(id) + .await + .map_err(|e| GraphQLError::new(e.to_string())) + } } diff --git a/src/main.rs b/src/main.rs index 74f4006..0aa2ba3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,10 @@ mod services; use app::create_router; use clap::Parser; use cli::{ - AddPolicyArgs, AssignRoleArgs, CheckPermissionArgs, Cli, Commands, ListRolePermissionsArgs, - ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands, RemovePolicyArgs, - RemoveRoleArgs, ServeArgs, + AddPolicyArgs, AssignRoleArgs, BlogArgs, BlogCommands, CheckPermissionArgs, Cli, Commands, + CreateBlogArgs, CreateCategoryArgs, CreateTagArgs, DeleteBlogArgs, ListBlogArgs, + ListRolePermissionsArgs, ListUserRolesArgs, MigrateArgs, PermissionsArgs, PermissionsCommands, + RemovePolicyArgs, RemoveRoleArgs, ServeArgs, ShowBlogArgs, UpdateBlogArgs, }; use config::Config; use db::{create_pool, run_migrations}; @@ -30,6 +31,7 @@ async fn main() -> Result<(), Box> { Commands::Serve(args) => serve_command(args).await, Commands::Migrate(args) => migrate_command(args).await, Commands::Permissions(args) => permissions_command(args).await, + Commands::Blog(args) => blog_command(args).await, Commands::Version => version_command(), Commands::Config => config_command(), } @@ -508,3 +510,419 @@ fn print_config_info(config: &Config, args: &ServeArgs) { ); println!(" 🗺️ 瓦片服务: {}", config.tile_server_url); } + +async fn blog_command(args: BlogArgs) -> Result<(), Box> { + use models::blog::*; + use models::page_block::PaginationInput; + use serde_json; + use services::blog_service::BlogService; + use uuid::Uuid; + + // 初始化配置和数据库连接 + let config = Config::from_env()?; + let pool = create_pool(&config.database_url).await?; + let blog_service = BlogService::new(pool); + + match args.command { + BlogCommands::Create(create_args) => { + println!("📝 创建博客文章..."); + + // 解析用户ID + let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?; + + // 解析分类ID + let category_id = if let Some(category_str) = create_args.category_id { + Some(Uuid::parse_str(&category_str).map_err(|_| "无效的分类ID格式")?) + } else { + None + }; + + // 解析标签ID列表 + let tag_ids = if let Some(tags_str) = create_args.tags { + let tags: Result, _> = tags_str + .split(',') + .map(|s| Uuid::parse_str(s.trim())) + .collect(); + Some(tags.map_err(|_| "无效的标签ID格式")?) + } else { + None + }; + + // 解析内容JSON + let content: serde_json::Value = + serde_json::from_str(&create_args.content).map_err(|_| "无效的JSON内容格式")?; + + let input = CreateBlogInput { + title: create_args.title.clone(), + slug: create_args.slug.clone(), + excerpt: create_args.excerpt.clone(), + content, + category_id, + status: Some(create_args.status.clone()), + featured_image: None, + meta_title: None, + meta_description: None, + is_featured: Some(create_args.featured), + is_active: Some(true), + tag_ids, + }; + + match blog_service.create_blog(input, user_id).await { + Ok(blog) => { + println!("✅ 博客文章创建成功!"); + println!(" 📄 ID: {}", blog.id); + println!(" 📝 标题: {}", blog.title); + println!(" 🔗 Slug: {}", blog.slug); + println!(" 📊 状态: {}", blog.status); + if create_args.featured { + println!(" ⭐ 推荐文章"); + } + } + Err(e) => { + eprintln!("❌ 创建失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::List(list_args) => { + println!("📋 博客文章列表"); + + // 构建过滤器 + let filter = + if list_args.status.is_some() || list_args.search.is_some() || list_args.featured { + Some(BlogFilterInput { + title: None, + slug: None, + category_id: None, + status: list_args.status, + is_featured: if list_args.featured { Some(true) } else { None }, + is_active: Some(true), + tag_ids: None, + search: list_args.search, + date_from: None, + date_to: None, + }) + } else { + None + }; + + // 分页参数 + let pagination = Some(PaginationInput { + page: Some(list_args.page), + per_page: Some(list_args.limit), + }); + + match blog_service.get_blogs(filter, None, pagination).await { + Ok(result) => { + println!( + " 📊 总计: {} 篇文章 (第 {}/{} 页)", + result.total, result.page, result.total_pages + ); + println!(); + + for blog in result.items { + println!("📄 {} | {}", blog.title, blog.id); + println!(" 🔗 Slug: {}", blog.slug); + println!(" 📊 状态: {}", blog.status); + if blog.is_featured { + println!(" ⭐ 推荐"); + } + if let Some(excerpt) = &blog.excerpt { + let short_excerpt = if excerpt.len() > 100 { + format!("{}...", &excerpt[..100]) + } else { + excerpt.clone() + }; + println!(" 📝 摘要: {}", short_excerpt); + } + println!(" 📅 创建: {}", blog.created_at.format("%Y-%m-%d %H:%M")); + println!(); + } + } + Err(e) => { + eprintln!("❌ 获取列表失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::Show(show_args) => { + println!("📖 博客文章详情"); + + // 尝试解析为UUID,如果失败则当作slug处理 + let blog = if let Ok(id) = Uuid::parse_str(&show_args.id_or_slug) { + blog_service.get_blog_by_id(id).await + } else { + blog_service.get_blog_by_slug(&show_args.id_or_slug).await + }; + + match blog { + Ok(blog) => { + println!(" 📄 ID: {}", blog.id); + println!(" 📝 标题: {}", blog.title); + println!(" 🔗 Slug: {}", blog.slug); + println!(" 📊 状态: {}", blog.status); + if let Some(excerpt) = &blog.excerpt { + println!(" 📝 摘要: {}", excerpt); + } + if blog.is_featured { + println!(" ⭐ 推荐文章"); + } + println!(" 👀 浏览次数: {}", blog.view_count); + println!( + " 📅 创建时间: {}", + blog.created_at.format("%Y-%m-%d %H:%M:%S") + ); + println!( + " 📅 更新时间: {}", + blog.updated_at.format("%Y-%m-%d %H:%M:%S") + ); + + if show_args.detail { + // 显示详细信息,包括分类和标签 + match blog_service.get_blog_detail(blog.id).await { + Ok(detail) => { + if let Some(category) = detail.category { + println!(" 📁 分类: {} ({})", category.name, category.slug); + } + if !detail.tags.is_empty() { + let tag_names: Vec = + detail.tags.iter().map(|tag| tag.name.clone()).collect(); + println!(" 🏷️ 标签: {}", tag_names.join(", ")); + } + } + Err(e) => { + eprintln!("⚠️ 获取详细信息失败: {}", e); + } + } + } + + println!(); + println!("📄 内容:"); + println!("{}", serde_json::to_string_pretty(&blog.content)?); + } + Err(e) => { + eprintln!("❌ 未找到博客文章: {}", e); + process::exit(1); + } + } + } + + BlogCommands::Update(update_args) => { + println!("✏️ 更新博客文章..."); + + let blog_id = Uuid::parse_str(&update_args.id).map_err(|_| "无效的博客ID格式")?; + let user_id = Uuid::parse_str(&update_args.user_id).map_err(|_| "无效的用户ID格式")?; + + // 解析分类ID + let category_id = if let Some(category_str) = update_args.category_id { + Some(Uuid::parse_str(&category_str).map_err(|_| "无效的分类ID格式")?) + } else { + None + }; + + // 解析标签ID列表 + let tag_ids = if let Some(tags_str) = update_args.tags { + let tags: Result, _> = tags_str + .split(',') + .map(|s| Uuid::parse_str(s.trim())) + .collect(); + Some(tags.map_err(|_| "无效的标签ID格式")?) + } else { + None + }; + + // 解析内容JSON + let content = if let Some(content_str) = update_args.content { + Some(serde_json::from_str(&content_str).map_err(|_| "无效的JSON内容格式")?) + } else { + None + }; + + let input = UpdateBlogInput { + title: update_args.title, + slug: update_args.slug, + excerpt: update_args.excerpt, + content, + category_id, + status: update_args.status, + featured_image: None, + meta_title: None, + meta_description: None, + is_featured: update_args.featured, + is_active: None, + tag_ids, + }; + + match blog_service.update_blog(blog_id, input, user_id).await { + Ok(blog) => { + println!("✅ 博客文章更新成功!"); + println!(" 📄 ID: {}", blog.id); + println!(" 📝 标题: {}", blog.title); + println!( + " 📅 更新时间: {}", + blog.updated_at.format("%Y-%m-%d %H:%M:%S") + ); + } + Err(e) => { + eprintln!("❌ 更新失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::Delete(delete_args) => { + println!("🗑️ 删除博客文章..."); + + let blog_id = Uuid::parse_str(&delete_args.id).map_err(|_| "无效的博客ID格式")?; + + match blog_service.delete_blog(blog_id).await { + Ok(true) => { + println!("✅ 博客文章删除成功!"); + } + Ok(false) => { + eprintln!("❌ 博客文章不存在"); + process::exit(1); + } + Err(e) => { + eprintln!("❌ 删除失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::CreateCategory(create_args) => { + println!("📁 创建博客分类..."); + + let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?; + + let input = CreateBlogCategoryInput { + name: create_args.name.clone(), + slug: create_args.slug.clone(), + description: create_args.description.clone(), + color: create_args.color.clone(), + icon: create_args.icon.clone(), + is_active: Some(true), + sort_order: Some(create_args.sort_order), + }; + + match blog_service.create_category(input, user_id).await { + Ok(category) => { + println!("✅ 博客分类创建成功!"); + println!(" 📁 ID: {}", category.id); + println!(" 📝 名称: {}", category.name); + println!(" 🔗 Slug: {}", category.slug); + if let Some(desc) = &category.description { + println!(" 📄 描述: {}", desc); + } + } + Err(e) => { + eprintln!("❌ 创建分类失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::ListCategories => { + println!("📁 博客分类列表"); + + match blog_service.get_categories(None).await { + Ok(categories) => { + for category in categories { + println!("📁 {} | {}", category.name, category.id); + println!(" 🔗 Slug: {}", category.slug); + if let Some(desc) = &category.description { + println!(" 📄 描述: {}", desc); + } + if let Some(color) = &category.color { + println!(" 🎨 颜色: {}", color); + } + println!(" 📊 排序: {}", category.sort_order); + println!(); + } + } + Err(e) => { + eprintln!("❌ 获取分类列表失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::CreateTag(create_args) => { + println!("🏷️ 创建博客标签..."); + + let user_id = Uuid::parse_str(&create_args.user_id).map_err(|_| "无效的用户ID格式")?; + + let input = CreateBlogTagInput { + name: create_args.name.clone(), + slug: create_args.slug.clone(), + description: create_args.description.clone(), + color: create_args.color.clone(), + is_active: Some(true), + }; + + match blog_service.create_tag(input, user_id).await { + Ok(tag) => { + println!("✅ 博客标签创建成功!"); + println!(" 🏷️ ID: {}", tag.id); + println!(" 📝 名称: {}", tag.name); + println!(" 🔗 Slug: {}", tag.slug); + if let Some(desc) = &tag.description { + println!(" 📄 描述: {}", desc); + } + } + Err(e) => { + eprintln!("❌ 创建标签失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::ListTags => { + println!("🏷️ 博客标签列表"); + + match blog_service.get_tags(None).await { + Ok(tags) => { + for tag in tags { + println!("🏷️ {} | {}", tag.name, tag.id); + println!(" 🔗 Slug: {}", tag.slug); + if let Some(desc) = &tag.description { + println!(" 📄 描述: {}", desc); + } + if let Some(color) = &tag.color { + println!(" 🎨 颜色: {}", color); + } + println!(); + } + } + Err(e) => { + eprintln!("❌ 获取标签列表失败: {}", e); + process::exit(1); + } + } + } + + BlogCommands::Stats => { + println!("📊 博客统计信息"); + + match blog_service.get_blog_stats().await { + Ok(stats) => { + println!(" 📄 总文章数: {}", stats.total_blogs); + println!(" ✅ 已发布: {}", stats.published_blogs); + println!(" 📝 草稿: {}", stats.draft_blogs); + println!(" 📦 归档: {}", stats.archived_blogs); + println!(" 📁 分类数: {}", stats.total_categories); + println!(" 🏷️ 标签数: {}", stats.total_tags); + println!(" 👀 总浏览量: {}", stats.total_views); + } + Err(e) => { + eprintln!("❌ 获取统计信息失败: {}", e); + process::exit(1); + } + } + } + } + + Ok(()) +} diff --git a/src/models/blog.rs b/src/models/blog.rs new file mode 100644 index 0000000..7a9abaf --- /dev/null +++ b/src/models/blog.rs @@ -0,0 +1,387 @@ +use async_graphql::{InputObject, SimpleObject}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +/// 博客分类模型 +#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +pub struct BlogCategory { + pub id: Uuid, + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: bool, + pub sort_order: i32, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, +} + +/// 博客标签模型 +#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +pub struct BlogTag { + pub id: Uuid, + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub is_active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, +} + +/// 博客文章模型 +#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +pub struct Blog { + pub id: Uuid, + pub title: String, + pub slug: String, + pub excerpt: Option, + pub content: serde_json::Value, + pub category_id: Option, + pub status: String, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub published_at: Option>, + pub view_count: i32, + pub is_featured: bool, + pub is_active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub created_by: Option, + pub updated_by: Option, +} + +/// 博客文章详情(包含分类和标签信息) +#[derive(Debug, Clone, Serialize, Deserialize, SimpleObject)] +pub struct BlogDetail { + #[graphql(flatten)] + pub blog: Blog, + pub category: Option, + pub tags: Vec, +} + +/// 博客标签关联模型 +#[derive(Debug, Clone, Serialize, Deserialize, FromRow, SimpleObject)] +pub struct BlogTagRelation { + pub id: Uuid, + pub blog_id: Uuid, + pub tag_id: Uuid, + pub created_at: DateTime, +} + +// 创建博客分类输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct CreateBlogCategoryInput { + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: Option, + pub sort_order: Option, +} + +// 更新博客分类输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct UpdateBlogCategoryInput { + pub name: Option, + pub slug: Option, + pub description: Option, + pub color: Option, + pub icon: Option, + pub is_active: Option, + pub sort_order: Option, +} + +// 创建博客标签输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct CreateBlogTagInput { + pub name: String, + pub slug: String, + pub description: Option, + pub color: Option, + pub is_active: Option, +} + +// 更新博客标签输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct UpdateBlogTagInput { + pub name: Option, + pub slug: Option, + pub description: Option, + pub color: Option, + pub is_active: Option, +} + +// 创建博客文章输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct CreateBlogInput { + pub title: String, + pub slug: String, + pub excerpt: Option, + pub content: serde_json::Value, + pub category_id: Option, + pub status: Option, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, +} + +// 更新博客文章输入 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct UpdateBlogInput { + pub title: Option, + pub slug: Option, + pub excerpt: Option, + pub content: Option, + pub category_id: Option, + pub status: Option, + pub featured_image: Option, + pub meta_title: Option, + pub meta_description: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, +} + +// 博客过滤器 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct BlogFilterInput { + pub title: Option, + pub slug: Option, + pub category_id: Option, + pub status: Option, + pub is_featured: Option, + pub is_active: Option, + pub tag_ids: Option>, + pub search: Option, + pub date_from: Option>, + pub date_to: Option>, +} + +// 博客分类过滤器 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct BlogCategoryFilterInput { + pub name: Option, + pub slug: Option, + pub is_active: Option, + pub search: Option, +} + +// 博客标签过滤器 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct BlogTagFilterInput { + pub name: Option, + pub slug: Option, + pub is_active: Option, + pub search: Option, +} + +// 博客排序 +#[derive(Debug, Clone, Serialize, Deserialize, InputObject)] +pub struct BlogSortInput { + pub field: String, + pub direction: String, // "asc" or "desc" +} + +// 博客统计信息 +#[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, +} + +impl BlogCategory { + /// 创建新博客分类 + pub fn new(input: CreateBlogCategoryInput, user_id: Uuid) -> Self { + Self { + id: Uuid::new_v4(), + name: input.name, + slug: input.slug, + description: input.description, + color: input.color, + icon: input.icon, + is_active: input.is_active.unwrap_or(true), + sort_order: input.sort_order.unwrap_or(0), + created_at: Utc::now(), + updated_at: Utc::now(), + created_by: Some(user_id), + updated_by: Some(user_id), + } + } + + /// 更新博客分类 + pub fn update(&mut self, input: UpdateBlogCategoryInput, user_id: Uuid) { + if let Some(name) = input.name { + self.name = name; + } + if let Some(slug) = input.slug { + self.slug = slug; + } + if let Some(description) = input.description { + self.description = Some(description); + } + if let Some(color) = input.color { + self.color = Some(color); + } + if let Some(icon) = input.icon { + self.icon = Some(icon); + } + if let Some(is_active) = input.is_active { + self.is_active = is_active; + } + if let Some(sort_order) = input.sort_order { + self.sort_order = sort_order; + } + self.updated_at = Utc::now(); + self.updated_by = Some(user_id); + } +} + +impl BlogTag { + /// 创建新博客标签 + pub fn new(input: CreateBlogTagInput, user_id: Uuid) -> Self { + Self { + id: Uuid::new_v4(), + name: input.name, + slug: input.slug, + description: input.description, + color: input.color, + is_active: input.is_active.unwrap_or(true), + created_at: Utc::now(), + updated_at: Utc::now(), + created_by: Some(user_id), + updated_by: Some(user_id), + } + } + + /// 更新博客标签 + pub fn update(&mut self, input: UpdateBlogTagInput, user_id: Uuid) { + if let Some(name) = input.name { + self.name = name; + } + if let Some(slug) = input.slug { + self.slug = slug; + } + if let Some(description) = input.description { + self.description = Some(description); + } + if let Some(color) = input.color { + self.color = Some(color); + } + if let Some(is_active) = input.is_active { + self.is_active = is_active; + } + self.updated_at = Utc::now(); + self.updated_by = Some(user_id); + } +} + +impl Blog { + /// 创建新博客文章 + pub fn new(input: CreateBlogInput, user_id: Uuid) -> Self { + let now = Utc::now(); + let published_at = if input.status.as_deref() == Some("published") { + Some(now) + } else { + None + }; + + Self { + id: Uuid::new_v4(), + title: input.title, + slug: input.slug, + excerpt: input.excerpt, + content: input.content, + category_id: input.category_id, + status: input.status.unwrap_or_else(|| "draft".to_string()), + featured_image: input.featured_image, + meta_title: input.meta_title, + meta_description: input.meta_description, + published_at, + view_count: 0, + is_featured: input.is_featured.unwrap_or(false), + is_active: input.is_active.unwrap_or(true), + created_at: now, + updated_at: now, + created_by: Some(user_id), + updated_by: Some(user_id), + } + } + + /// 更新博客文章 + pub fn update(&mut self, input: UpdateBlogInput, user_id: Uuid) { + if let Some(title) = input.title { + self.title = title; + } + if let Some(slug) = input.slug { + self.slug = slug; + } + if let Some(excerpt) = input.excerpt { + self.excerpt = Some(excerpt); + } + if let Some(content) = input.content { + self.content = content; + } + if let Some(category_id) = input.category_id { + self.category_id = Some(category_id); + } + if let Some(status) = input.status { + self.status = status.clone(); + // 如果状态变为published且之前未发布,设置发布时间 + if status == "published" && self.published_at.is_none() { + self.published_at = Some(Utc::now()); + } + } + if let Some(featured_image) = input.featured_image { + self.featured_image = Some(featured_image); + } + if let Some(meta_title) = input.meta_title { + self.meta_title = Some(meta_title); + } + if let Some(meta_description) = input.meta_description { + self.meta_description = Some(meta_description); + } + if let Some(is_featured) = input.is_featured { + self.is_featured = is_featured; + } + if let Some(is_active) = input.is_active { + self.is_active = is_active; + } + self.updated_at = Utc::now(); + self.updated_by = Some(user_id); + } + + /// 增加浏览次数 + pub fn increment_view_count(&mut self) { + self.view_count += 1; + self.updated_at = Utc::now(); + } +} + +impl BlogDetail { + /// 从博客文章创建详情对象 + pub fn new(blog: Blog, category: Option, tags: Vec) -> Self { + Self { + blog, + category, + tags, + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 7a027d5..49425d9 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,9 +1,11 @@ +pub mod blog; pub mod invite_code; pub mod kafka_message; pub mod page_block; pub mod settings; pub mod user; +pub use blog::*; pub use invite_code::*; pub use kafka_message::*; pub use page_block::*; diff --git a/src/services/blog_service.rs b/src/services/blog_service.rs new file mode 100644 index 0000000..6634bf6 --- /dev/null +++ b/src/services/blog_service.rs @@ -0,0 +1,1140 @@ +use anyhow::{anyhow, Result}; +use chrono::Utc; +use sea_query::{extension::postgres::PgExpr, Expr, Iden, Order, PostgresQueryBuilder, Query}; +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 { + Table, + Id, + Title, + Slug, + Excerpt, + Content, + CategoryId, + Status, + FeaturedImage, + MetaTitle, + MetaDescription, + PublishedAt, + ViewCount, + IsFeatured, + IsActive, + CreatedAt, + UpdatedAt, + CreatedBy, + UpdatedBy, +} + +impl Iden for BlogsIden { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!( + s, + "{}", + match self { + Self::Table => "blogs", + Self::Id => "id", + Self::Title => "title", + Self::Slug => "slug", + Self::Excerpt => "excerpt", + Self::Content => "content", + Self::CategoryId => "category_id", + Self::Status => "status", + Self::FeaturedImage => "featured_image", + Self::MetaTitle => "meta_title", + Self::MetaDescription => "meta_description", + Self::PublishedAt => "published_at", + Self::ViewCount => "view_count", + Self::IsFeatured => "is_featured", + Self::IsActive => "is_active", + Self::CreatedAt => "created_at", + Self::UpdatedAt => "updated_at", + Self::CreatedBy => "created_by", + Self::UpdatedBy => "updated_by", + } + ) + .unwrap(); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum BlogCategoriesIden { + Table, + Id, + Name, + Slug, + Description, + Color, + Icon, + IsActive, + SortOrder, + CreatedAt, + UpdatedAt, + CreatedBy, + UpdatedBy, +} + +impl Iden for BlogCategoriesIden { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!( + s, + "{}", + match self { + Self::Table => "blog_categories", + Self::Id => "id", + Self::Name => "name", + Self::Slug => "slug", + Self::Description => "description", + Self::Color => "color", + Self::Icon => "icon", + Self::IsActive => "is_active", + Self::SortOrder => "sort_order", + Self::CreatedAt => "created_at", + Self::UpdatedAt => "updated_at", + Self::CreatedBy => "created_by", + Self::UpdatedBy => "updated_by", + } + ) + .unwrap(); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum BlogTagsIden { + Table, + Id, + Name, + Slug, + Description, + Color, + IsActive, + CreatedAt, + UpdatedAt, + CreatedBy, + UpdatedBy, +} + +impl Iden for BlogTagsIden { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!( + s, + "{}", + match self { + Self::Table => "blog_tags", + Self::Id => "id", + Self::Name => "name", + Self::Slug => "slug", + Self::Description => "description", + Self::Color => "color", + Self::IsActive => "is_active", + Self::CreatedAt => "created_at", + Self::UpdatedAt => "updated_at", + Self::CreatedBy => "created_by", + Self::UpdatedBy => "updated_by", + } + ) + .unwrap(); + } +} + +#[derive(Debug, Clone, Copy)] +pub enum BlogTagRelationsIden { + Table, + Id, + BlogId, + TagId, + CreatedAt, +} + +impl Iden for BlogTagRelationsIden { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!( + s, + "{}", + match self { + Self::Table => "blog_tag_relations", + Self::Id => "id", + Self::BlogId => "blog_id", + Self::TagId => "tag_id", + Self::CreatedAt => "created_at", + } + ) + .unwrap(); + } +} + +/// 博客服务 +pub struct BlogService { + pool: PgPool, + query_builder: DynamicQueryBuilder, +} + +impl BlogService { + pub fn new(pool: PgPool) -> Self { + let query_builder = DynamicQueryBuilder::new(pool.clone()); + Self { + pool, + query_builder, + } + } + + // ==================== 博客文章相关方法 ==================== + + /// 创建博客文章 + pub async fn create_blog(&self, input: CreateBlogInput, user_id: Uuid) -> Result { + let mut tx = self.pool.begin().await?; + + let blog = Blog::new(input.clone(), user_id); + + // 插入博客文章 + let (sql, values) = Query::insert() + .into_table(BlogsIden::Table) + .columns([ + BlogsIden::Id, + BlogsIden::Title, + BlogsIden::Slug, + BlogsIden::Excerpt, + BlogsIden::Content, + BlogsIden::CategoryId, + BlogsIden::Status, + BlogsIden::FeaturedImage, + BlogsIden::MetaTitle, + BlogsIden::MetaDescription, + BlogsIden::PublishedAt, + BlogsIden::ViewCount, + BlogsIden::IsFeatured, + BlogsIden::IsActive, + BlogsIden::CreatedAt, + BlogsIden::UpdatedAt, + BlogsIden::CreatedBy, + BlogsIden::UpdatedBy, + ]) + .values([ + blog.id.into(), + blog.title.clone().into(), + blog.slug.clone().into(), + blog.excerpt.clone().into(), + blog.content.clone().into(), + blog.category_id.into(), + blog.status.clone().into(), + blog.featured_image.clone().into(), + blog.meta_title.clone().into(), + blog.meta_description.clone().into(), + blog.published_at.into(), + blog.view_count.into(), + blog.is_featured.into(), + blog.is_active.into(), + blog.created_at.into(), + blog.updated_at.into(), + blog.created_by.into(), + blog.updated_by.into(), + ])? + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&mut *tx).await?; + + // 处理标签关联 + if let Some(tag_ids) = input.tag_ids { + for tag_id in tag_ids { + let (sql, values) = Query::insert() + .into_table(BlogTagRelationsIden::Table) + .columns([ + BlogTagRelationsIden::Id, + BlogTagRelationsIden::BlogId, + BlogTagRelationsIden::TagId, + BlogTagRelationsIden::CreatedAt, + ]) + .values([ + Uuid::new_v4().into(), + blog.id.into(), + tag_id.into(), + Utc::now().into(), + ])? + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&mut *tx).await?; + } + } + + tx.commit().await?; + Ok(blog) + } + + /// 更新博客文章 + pub async fn update_blog( + &self, + id: Uuid, + input: UpdateBlogInput, + user_id: Uuid, + ) -> Result { + let mut tx = self.pool.begin().await?; + + // 获取现有博客文章 + let mut blog = self.get_blog_by_id(id).await?; + blog.update(input.clone(), user_id); + + // 更新博客文章 + let mut update_query = Query::update(); + update_query.table(BlogsIden::Table); + + if let Some(title) = &input.title { + update_query.value(BlogsIden::Title, title.clone()); + } + if let Some(slug) = &input.slug { + update_query.value(BlogsIden::Slug, slug.clone()); + } + if let Some(excerpt) = &input.excerpt { + update_query.value(BlogsIden::Excerpt, excerpt.clone()); + } + if let Some(content) = &input.content { + update_query.value(BlogsIden::Content, content.clone()); + } + if let Some(category_id) = input.category_id { + update_query.value(BlogsIden::CategoryId, category_id); + } + if let Some(status) = &input.status { + update_query.value(BlogsIden::Status, status.clone()); + if status == "published" && blog.published_at.is_none() { + update_query.value(BlogsIden::PublishedAt, Utc::now()); + } + } + if let Some(featured_image) = &input.featured_image { + update_query.value(BlogsIden::FeaturedImage, featured_image.clone()); + } + if let Some(meta_title) = &input.meta_title { + update_query.value(BlogsIden::MetaTitle, meta_title.clone()); + } + if let Some(meta_description) = &input.meta_description { + update_query.value(BlogsIden::MetaDescription, meta_description.clone()); + } + if let Some(is_featured) = input.is_featured { + update_query.value(BlogsIden::IsFeatured, is_featured); + } + if let Some(is_active) = input.is_active { + update_query.value(BlogsIden::IsActive, is_active); + } + + update_query + .value(BlogsIden::UpdatedAt, Utc::now()) + .value(BlogsIden::UpdatedBy, user_id) + .and_where(Expr::col(BlogsIden::Id).eq(id)); + + let (sql, values) = update_query.build_sqlx(PostgresQueryBuilder); + let result = sqlx::query_with(&sql, values).execute(&mut *tx).await?; + + if result.rows_affected() == 0 { + return Err(anyhow!("博客文章不存在")); + } + + // 处理标签关联 + if let Some(tag_ids) = input.tag_ids { + // 删除现有关联 + let (sql, values) = Query::delete() + .from_table(BlogTagRelationsIden::Table) + .and_where(Expr::col(BlogTagRelationsIden::BlogId).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&mut *tx).await?; + + // 添加新关联 + for tag_id in tag_ids { + let (sql, values) = Query::insert() + .into_table(BlogTagRelationsIden::Table) + .columns([ + BlogTagRelationsIden::Id, + BlogTagRelationsIden::BlogId, + BlogTagRelationsIden::TagId, + BlogTagRelationsIden::CreatedAt, + ]) + .values([ + Uuid::new_v4().into(), + id.into(), + tag_id.into(), + Utc::now().into(), + ])? + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&mut *tx).await?; + } + } + + tx.commit().await?; + self.get_blog_by_id(id).await + } + + /// 删除博客文章 + pub async fn delete_blog(&self, id: Uuid) -> Result { + let (sql, values) = Query::delete() + .from_table(BlogsIden::Table) + .and_where(Expr::col(BlogsIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(result.rows_affected() > 0) + } + + /// 根据ID获取博客文章 + pub async fn get_blog_by_id(&self, id: Uuid) -> Result { + let (sql, values) = Query::select() + .from(BlogsIden::Table) + .columns([ + BlogsIden::Id, + BlogsIden::Title, + BlogsIden::Slug, + BlogsIden::Excerpt, + BlogsIden::Content, + BlogsIden::CategoryId, + BlogsIden::Status, + BlogsIden::FeaturedImage, + BlogsIden::MetaTitle, + BlogsIden::MetaDescription, + BlogsIden::PublishedAt, + BlogsIden::ViewCount, + BlogsIden::IsFeatured, + BlogsIden::IsActive, + BlogsIden::CreatedAt, + BlogsIden::UpdatedAt, + BlogsIden::CreatedBy, + BlogsIden::UpdatedBy, + ]) + .and_where(Expr::col(BlogsIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let row = sqlx::query_with(&sql, values) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow!("博客文章不存在"))?; + + Ok(Blog::from_row(&row)?) + } + + /// 根据slug获取博客文章 + pub async fn get_blog_by_slug(&self, slug: &str) -> Result { + let (sql, values) = Query::select() + .from(BlogsIden::Table) + .columns([ + BlogsIden::Id, + BlogsIden::Title, + BlogsIden::Slug, + BlogsIden::Excerpt, + BlogsIden::Content, + BlogsIden::CategoryId, + BlogsIden::Status, + BlogsIden::FeaturedImage, + BlogsIden::MetaTitle, + BlogsIden::MetaDescription, + BlogsIden::PublishedAt, + BlogsIden::ViewCount, + BlogsIden::IsFeatured, + BlogsIden::IsActive, + BlogsIden::CreatedAt, + BlogsIden::UpdatedAt, + BlogsIden::CreatedBy, + BlogsIden::UpdatedBy, + ]) + .and_where(Expr::col(BlogsIden::Slug).eq(slug)) + .build_sqlx(PostgresQueryBuilder); + + let row = sqlx::query_with(&sql, values) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow!("博客文章不存在"))?; + + Ok(Blog::from_row(&row)?) + } + + /// 获取博客文章列表(支持过滤、排序和分页) + pub async fn get_blogs( + &self, + filter: Option, + sort: Option, + pagination: Option, + ) -> Result> { + let page = pagination.as_ref().and_then(|p| p.page).unwrap_or(1); + let per_page = pagination.as_ref().and_then(|p| p.per_page).unwrap_or(10); + let offset = (page - 1) * per_page; + + let mut query = Query::select(); + query.from(BlogsIden::Table); + query.columns([ + BlogsIden::Id, + BlogsIden::Title, + BlogsIden::Slug, + BlogsIden::Excerpt, + BlogsIden::Content, + BlogsIden::CategoryId, + BlogsIden::Status, + BlogsIden::FeaturedImage, + BlogsIden::MetaTitle, + BlogsIden::MetaDescription, + BlogsIden::PublishedAt, + BlogsIden::ViewCount, + BlogsIden::IsFeatured, + BlogsIden::IsActive, + BlogsIden::CreatedAt, + BlogsIden::UpdatedAt, + BlogsIden::CreatedBy, + BlogsIden::UpdatedBy, + ]); + + // 应用过滤条件 + if let Some(ref filter) = filter { + if let Some(ref title) = filter.title { + query.and_where(Expr::col(BlogsIden::Title).ilike(format!("%{}%", title))); + } + if let Some(ref slug) = filter.slug { + query.and_where(Expr::col(BlogsIden::Slug).eq(slug)); + } + if let Some(category_id) = filter.category_id { + query.and_where(Expr::col(BlogsIden::CategoryId).eq(category_id)); + } + if let Some(ref status) = filter.status { + query.and_where(Expr::col(BlogsIden::Status).eq(status)); + } + if let Some(is_featured) = filter.is_featured { + query.and_where(Expr::col(BlogsIden::IsFeatured).eq(is_featured)); + } + if let Some(is_active) = filter.is_active { + query.and_where(Expr::col(BlogsIden::IsActive).eq(is_active)); + } + + // 搜索条件 + if let Some(ref search) = filter.search { + let search_pattern = format!("%{}%", search); + query.and_where( + Expr::col(BlogsIden::Title) + .ilike(&search_pattern) + .or(Expr::col(BlogsIden::Excerpt).ilike(&search_pattern)), + ); + } + + // 日期范围过滤 + if let Some(date_from) = filter.date_from { + query.and_where(Expr::col(BlogsIden::CreatedAt).gte(date_from)); + } + if let Some(date_to) = filter.date_to { + query.and_where(Expr::col(BlogsIden::CreatedAt).lte(date_to)); + } + + // 标签过滤 + if let Some(ref tag_ids) = filter.tag_ids { + if !tag_ids.is_empty() { + query.and_where( + Expr::col(BlogsIden::Id).in_subquery( + Query::select() + .column(BlogTagRelationsIden::BlogId) + .from(BlogTagRelationsIden::Table) + .and_where( + Expr::col(BlogTagRelationsIden::TagId).is_in(tag_ids.clone()), + ) + .to_owned(), + ), + ); + } + } + } + + // 应用排序 + if let Some(sort) = sort { + let order = if sort.direction.to_lowercase() == "desc" { + Order::Desc + } else { + Order::Asc + }; + + let sort_column = match sort.field.as_str() { + "title" => BlogsIden::Title, + "created_at" => BlogsIden::CreatedAt, + "updated_at" => BlogsIden::UpdatedAt, + "published_at" => BlogsIden::PublishedAt, + "view_count" => BlogsIden::ViewCount, + _ => BlogsIden::CreatedAt, + }; + + query.order_by(sort_column, order); + } else { + query.order_by(BlogsIden::CreatedAt, Order::Desc); + } + + // 应用分页 + query.limit(per_page as u64).offset(offset as u64); + + // 执行查询 + let (sql, values) = query.build_sqlx(PostgresQueryBuilder); + let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; + + let blogs: Result, _> = rows.iter().map(|row| Blog::from_row(row)).collect(); + let blogs = blogs?; + + // 获取总数 + let filter_for_count = filter.clone(); + let total = self.count_blogs(filter_for_count).await?; + let total_pages = (total as f64 / per_page as f64).ceil() as i32; + + Ok(PaginatedResult::new(blogs, total, page, per_page)) + } + + /// 统计博客文章数量 + async fn count_blogs(&self, filter: Option) -> Result { + let mut query = Query::select(); + query.from(BlogsIden::Table); + query.expr(Expr::col(BlogsIden::Id).count()); + + // 应用过滤条件 + if let Some(filter) = filter { + if let Some(title) = filter.title { + query.and_where(Expr::col(BlogsIden::Title).ilike(format!("%{}%", title))); + } + if let Some(ref slug) = filter.slug { + query.and_where(Expr::col(BlogsIden::Slug).eq(slug)); + } + if let Some(category_id) = filter.category_id { + query.and_where(Expr::col(BlogsIden::CategoryId).eq(category_id)); + } + if let Some(status) = filter.status { + query.and_where(Expr::col(BlogsIden::Status).eq(status)); + } + if let Some(is_featured) = filter.is_featured { + query.and_where(Expr::col(BlogsIden::IsFeatured).eq(is_featured)); + } + if let Some(is_active) = filter.is_active { + query.and_where(Expr::col(BlogsIden::IsActive).eq(is_active)); + } + + // 搜索条件 + if let Some(ref search) = filter.search { + let search_pattern = format!("%{}%", search); + query.and_where( + Expr::col(BlogsIden::Title) + .ilike(&search_pattern) + .or(Expr::col(BlogsIden::Excerpt).ilike(&search_pattern)), + ); + } + + // 日期范围过滤 + if let Some(date_from) = filter.date_from { + query.and_where(Expr::col(BlogsIden::CreatedAt).gte(date_from)); + } + if let Some(date_to) = filter.date_to { + query.and_where(Expr::col(BlogsIden::CreatedAt).lte(date_to)); + } + + // 标签过滤 + if let Some(ref tag_ids) = filter.tag_ids { + if !tag_ids.is_empty() { + query.and_where( + Expr::col(BlogsIden::Id).in_subquery( + Query::select() + .column(BlogTagRelationsIden::BlogId) + .from(BlogTagRelationsIden::Table) + .and_where( + Expr::col(BlogTagRelationsIden::TagId).is_in(tag_ids.clone()), + ) + .to_owned(), + ), + ); + } + } + } + + let (sql, values) = query.build_sqlx(PostgresQueryBuilder); + let count: i64 = sqlx::query_scalar_with(&sql, values) + .fetch_one(&self.pool) + .await?; + + Ok(count) + } + + /// 增加博客浏览次数 + pub async fn increment_blog_view_count(&self, id: Uuid) -> Result<()> { + let (sql, values) = Query::update() + .table(BlogsIden::Table) + .value(BlogsIden::ViewCount, Expr::col(BlogsIden::ViewCount).add(1)) + .value(BlogsIden::UpdatedAt, Utc::now()) + .and_where(Expr::col(BlogsIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(()) + } + + /// 获取博客文章详情(包含分类和标签) + pub async fn get_blog_detail(&self, id: Uuid) -> Result { + let blog = self.get_blog_by_id(id).await?; + + // 获取分类信息 + let category = if let Some(category_id) = blog.category_id { + self.get_category_by_id(category_id).await.ok() + } else { + None + }; + + // 获取标签信息 + let tags = self.get_tags_by_blog_id(id).await?; + + Ok(BlogDetail::new(blog, category, tags)) + } + + /// 根据博客ID获取标签列表 + async fn get_tags_by_blog_id(&self, blog_id: Uuid) -> Result> { + let (sql, values) = Query::select() + .from(BlogTagsIden::Table) + .inner_join( + BlogTagRelationsIden::Table, + Expr::col((BlogTagsIden::Table, BlogTagsIden::Id)) + .equals((BlogTagRelationsIden::Table, BlogTagRelationsIden::TagId)), + ) + .columns([ + (BlogTagsIden::Table, BlogTagsIden::Id), + (BlogTagsIden::Table, BlogTagsIden::Name), + (BlogTagsIden::Table, BlogTagsIden::Slug), + (BlogTagsIden::Table, BlogTagsIden::Description), + (BlogTagsIden::Table, BlogTagsIden::Color), + (BlogTagsIden::Table, BlogTagsIden::IsActive), + (BlogTagsIden::Table, BlogTagsIden::CreatedAt), + (BlogTagsIden::Table, BlogTagsIden::UpdatedAt), + (BlogTagsIden::Table, BlogTagsIden::CreatedBy), + (BlogTagsIden::Table, BlogTagsIden::UpdatedBy), + ]) + .and_where( + Expr::col((BlogTagRelationsIden::Table, BlogTagRelationsIden::BlogId)).eq(blog_id), + ) + .build_sqlx(PostgresQueryBuilder); + + let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; + let tags: Result, _> = rows.iter().map(|row| BlogTag::from_row(row)).collect(); + Ok(tags?) + } + + /// 获取博客统计信息 + pub async fn get_blog_stats(&self) -> Result { + // 总博客数 + let total_blogs: i64 = + sqlx::query_scalar!("SELECT COUNT(*) FROM blogs WHERE is_active = true") + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 已发布博客数 + let published_blogs: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) FROM blogs WHERE status = 'published' AND is_active = true" + ) + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 草稿博客数 + let draft_blogs: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) FROM blogs WHERE status = 'draft' AND is_active = true" + ) + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 归档博客数 + let archived_blogs: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) FROM blogs WHERE status = 'archived' AND is_active = true" + ) + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 总分类数 + let total_categories: i64 = + sqlx::query_scalar!("SELECT COUNT(*) FROM blog_categories WHERE is_active = true") + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 总标签数 + let total_tags: i64 = + sqlx::query_scalar!("SELECT COUNT(*) FROM blog_tags WHERE is_active = true") + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + // 总浏览数 + let total_views: i64 = sqlx::query_scalar!( + "SELECT COALESCE(SUM(view_count), 0) FROM blogs WHERE is_active = true" + ) + .fetch_one(&self.pool) + .await? + .unwrap_or(0); + + Ok(BlogStats { + total_blogs, + published_blogs, + draft_blogs, + archived_blogs, + total_categories, + total_tags, + total_views, + }) + } + + // ==================== 博客分类相关方法 ==================== + + /// 创建博客分类 + pub async fn create_category( + &self, + input: CreateBlogCategoryInput, + user_id: Uuid, + ) -> Result { + let category = BlogCategory::new(input, user_id); + + let (sql, values) = Query::insert() + .into_table(BlogCategoriesIden::Table) + .columns([ + BlogCategoriesIden::Id, + BlogCategoriesIden::Name, + BlogCategoriesIden::Slug, + BlogCategoriesIden::Description, + BlogCategoriesIden::Color, + BlogCategoriesIden::Icon, + BlogCategoriesIden::IsActive, + BlogCategoriesIden::SortOrder, + BlogCategoriesIden::CreatedAt, + BlogCategoriesIden::UpdatedAt, + BlogCategoriesIden::CreatedBy, + BlogCategoriesIden::UpdatedBy, + ]) + .values([ + category.id.into(), + category.name.clone().into(), + category.slug.clone().into(), + category.description.clone().into(), + category.color.clone().into(), + category.icon.clone().into(), + category.is_active.into(), + category.sort_order.into(), + category.created_at.into(), + category.updated_at.into(), + category.created_by.into(), + category.updated_by.into(), + ])? + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(category) + } + + /// 更新博客分类 + pub async fn update_category( + &self, + id: Uuid, + input: UpdateBlogCategoryInput, + user_id: Uuid, + ) -> Result { + let mut category = self.get_category_by_id(id).await?; + category.update(input.clone(), user_id); + + let mut update_query = Query::update(); + update_query.table(BlogCategoriesIden::Table); + + if let Some(name) = &input.name { + update_query.value(BlogCategoriesIden::Name, name.clone()); + } + if let Some(slug) = &input.slug { + update_query.value(BlogCategoriesIden::Slug, slug.clone()); + } + if let Some(description) = &input.description { + update_query.value(BlogCategoriesIden::Description, description.clone()); + } + if let Some(color) = &input.color { + update_query.value(BlogCategoriesIden::Color, color.clone()); + } + if let Some(icon) = &input.icon { + update_query.value(BlogCategoriesIden::Icon, icon.clone()); + } + if let Some(is_active) = input.is_active { + update_query.value(BlogCategoriesIden::IsActive, is_active); + } + if let Some(sort_order) = input.sort_order { + update_query.value(BlogCategoriesIden::SortOrder, sort_order); + } + + update_query + .value(BlogCategoriesIden::UpdatedAt, Utc::now()) + .value(BlogCategoriesIden::UpdatedBy, user_id) + .and_where(Expr::col(BlogCategoriesIden::Id).eq(id)); + + let (sql, values) = update_query.build_sqlx(PostgresQueryBuilder); + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + + if result.rows_affected() == 0 { + return Err(anyhow!("博客分类不存在")); + } + + Ok(category) + } + + /// 删除博客分类 + pub async fn delete_category(&self, id: Uuid) -> Result { + let (sql, values) = Query::delete() + .from_table(BlogCategoriesIden::Table) + .and_where(Expr::col(BlogCategoriesIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(result.rows_affected() > 0) + } + + /// 根据ID获取博客分类 + pub async fn get_category_by_id(&self, id: Uuid) -> Result { + let (sql, values) = Query::select() + .from(BlogCategoriesIden::Table) + .columns([ + BlogCategoriesIden::Id, + BlogCategoriesIden::Name, + BlogCategoriesIden::Slug, + BlogCategoriesIden::Description, + BlogCategoriesIden::Color, + BlogCategoriesIden::Icon, + BlogCategoriesIden::IsActive, + BlogCategoriesIden::SortOrder, + BlogCategoriesIden::CreatedAt, + BlogCategoriesIden::UpdatedAt, + BlogCategoriesIden::CreatedBy, + BlogCategoriesIden::UpdatedBy, + ]) + .and_where(Expr::col(BlogCategoriesIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let row = sqlx::query_with(&sql, values) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow!("博客分类不存在"))?; + + Ok(BlogCategory::from_row(&row)?) + } + + /// 获取博客分类列表 + pub async fn get_categories( + &self, + filter: Option, + ) -> Result> { + let mut query = Query::select(); + query.from(BlogCategoriesIden::Table); + query.columns([ + BlogCategoriesIden::Id, + BlogCategoriesIden::Name, + BlogCategoriesIden::Slug, + BlogCategoriesIden::Description, + BlogCategoriesIden::Color, + BlogCategoriesIden::Icon, + BlogCategoriesIden::IsActive, + BlogCategoriesIden::SortOrder, + BlogCategoriesIden::CreatedAt, + BlogCategoriesIden::UpdatedAt, + BlogCategoriesIden::CreatedBy, + BlogCategoriesIden::UpdatedBy, + ]); + + // 应用过滤条件 + if let Some(filter) = filter { + if let Some(ref name) = filter.name { + query.and_where(Expr::col(BlogCategoriesIden::Name).ilike(format!("%{}%", name))); + } + if let Some(ref slug) = filter.slug { + query.and_where(Expr::col(BlogCategoriesIden::Slug).eq(slug)); + } + if let Some(is_active) = filter.is_active { + query.and_where(Expr::col(BlogCategoriesIden::IsActive).eq(is_active)); + } + if let Some(ref search) = filter.search { + let search_pattern = format!("%{}%", search); + query.and_where( + Expr::col(BlogCategoriesIden::Name) + .ilike(&search_pattern) + .or(Expr::col(BlogCategoriesIden::Description).ilike(&search_pattern)), + ); + } + } + + // 按排序字段排序 + query.order_by(BlogCategoriesIden::SortOrder, Order::Asc); + query.order_by(BlogCategoriesIden::CreatedAt, Order::Desc); + + let (sql, values) = query.build_sqlx(PostgresQueryBuilder); + let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; + + let categories: Result, _> = + rows.iter().map(|row| BlogCategory::from_row(row)).collect(); + Ok(categories?) + } + + // ==================== 博客标签相关方法 ==================== + + /// 创建博客标签 + pub async fn create_tag(&self, input: CreateBlogTagInput, user_id: Uuid) -> Result { + let tag = BlogTag::new(input, user_id); + + let (sql, values) = Query::insert() + .into_table(BlogTagsIden::Table) + .columns([ + BlogTagsIden::Id, + BlogTagsIden::Name, + BlogTagsIden::Slug, + BlogTagsIden::Description, + BlogTagsIden::Color, + BlogTagsIden::IsActive, + BlogTagsIden::CreatedAt, + BlogTagsIden::UpdatedAt, + BlogTagsIden::CreatedBy, + BlogTagsIden::UpdatedBy, + ]) + .values([ + tag.id.into(), + tag.name.clone().into(), + tag.slug.clone().into(), + tag.description.clone().into(), + tag.color.clone().into(), + tag.is_active.into(), + tag.created_at.into(), + tag.updated_at.into(), + tag.created_by.into(), + tag.updated_by.into(), + ])? + .build_sqlx(PostgresQueryBuilder); + + sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(tag) + } + + /// 更新博客标签 + pub async fn update_tag( + &self, + id: Uuid, + input: UpdateBlogTagInput, + user_id: Uuid, + ) -> Result { + let mut tag = self.get_tag_by_id(id).await?; + tag.update(input.clone(), user_id); + + let mut update_query = Query::update(); + update_query.table(BlogTagsIden::Table); + + if let Some(name) = &input.name { + update_query.value(BlogTagsIden::Name, name.clone()); + } + if let Some(slug) = &input.slug { + update_query.value(BlogTagsIden::Slug, slug.clone()); + } + if let Some(description) = &input.description { + update_query.value(BlogTagsIden::Description, description.clone()); + } + if let Some(color) = &input.color { + update_query.value(BlogTagsIden::Color, color.clone()); + } + if let Some(is_active) = input.is_active { + update_query.value(BlogTagsIden::IsActive, is_active); + } + + update_query + .value(BlogTagsIden::UpdatedAt, Utc::now()) + .value(BlogTagsIden::UpdatedBy, user_id) + .and_where(Expr::col(BlogTagsIden::Id).eq(id)); + + let (sql, values) = update_query.build_sqlx(PostgresQueryBuilder); + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + + if result.rows_affected() == 0 { + return Err(anyhow!("博客标签不存在")); + } + + Ok(tag) + } + + /// 删除博客标签 + pub async fn delete_tag(&self, id: Uuid) -> Result { + let (sql, values) = Query::delete() + .from_table(BlogTagsIden::Table) + .and_where(Expr::col(BlogTagsIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let result = sqlx::query_with(&sql, values).execute(&self.pool).await?; + Ok(result.rows_affected() > 0) + } + + /// 根据ID获取博客标签 + pub async fn get_tag_by_id(&self, id: Uuid) -> Result { + let (sql, values) = Query::select() + .from(BlogTagsIden::Table) + .columns([ + BlogTagsIden::Id, + BlogTagsIden::Name, + BlogTagsIden::Slug, + BlogTagsIden::Description, + BlogTagsIden::Color, + BlogTagsIden::IsActive, + BlogTagsIden::CreatedAt, + BlogTagsIden::UpdatedAt, + BlogTagsIden::CreatedBy, + BlogTagsIden::UpdatedBy, + ]) + .and_where(Expr::col(BlogTagsIden::Id).eq(id)) + .build_sqlx(PostgresQueryBuilder); + + let row = sqlx::query_with(&sql, values) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| anyhow!("博客标签不存在"))?; + + Ok(BlogTag::from_row(&row)?) + } + + /// 获取博客标签列表 + pub async fn get_tags(&self, filter: Option) -> Result> { + let mut query = Query::select(); + query.from(BlogTagsIden::Table); + query.columns([ + BlogTagsIden::Id, + BlogTagsIden::Name, + BlogTagsIden::Slug, + BlogTagsIden::Description, + BlogTagsIden::Color, + BlogTagsIden::IsActive, + BlogTagsIden::CreatedAt, + BlogTagsIden::UpdatedAt, + BlogTagsIden::CreatedBy, + BlogTagsIden::UpdatedBy, + ]); + + // 应用过滤条件 + if let Some(filter) = filter { + if let Some(ref name) = filter.name { + query.and_where(Expr::col(BlogTagsIden::Name).ilike(format!("%{}%", name))); + } + if let Some(ref slug) = filter.slug { + query.and_where(Expr::col(BlogTagsIden::Slug).eq(slug)); + } + if let Some(is_active) = filter.is_active { + query.and_where(Expr::col(BlogTagsIden::IsActive).eq(is_active)); + } + if let Some(ref search) = filter.search { + let search_pattern = format!("%{}%", search); + query.and_where( + Expr::col(BlogTagsIden::Name) + .ilike(&search_pattern) + .or(Expr::col(BlogTagsIden::Description).ilike(&search_pattern)), + ); + } + } + + // 按名称排序 + query.order_by(BlogTagsIden::Name, Order::Asc); + + let (sql, values) = query.build_sqlx(PostgresQueryBuilder); + let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?; + + let tags: Result, _> = rows.iter().map(|row| BlogTag::from_row(row)).collect(); + Ok(tags?) + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index cc1af30..a55a537 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,3 +1,4 @@ +pub mod blog_service; pub mod casbin_service; pub mod invite_code_service; pub mod mosaic_service; diff --git a/test_blog b/test_blog new file mode 100644 index 0000000..fa414ff --- /dev/null +++ b/test_blog @@ -0,0 +1,477 @@ +{ + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 1 + }, + "content": [ + { + "type": "text", + "text": "Getting started" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Welcome to the " + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + }, + { + "type": "highlight", + "attrs": { + "color": "var(--tt-color-highlight-yellow)" + } + } + ], + "text": "Simple Editor" + }, + { + "type": "text", + "text": " template! This template integrates " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "open source" + }, + { + "type": "text", + "text": " UI components and Tiptap extensions licensed under " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "MIT" + }, + { + "type": "text", + "text": "." + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "text": "Integrate it by following the " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Tiptap UI Components docs" + }, + { + "type": "text", + "text": " or using our CLI tool." + } + ] + }, + { + "type": "codeBlock", + "attrs": { + "language": null + }, + "content": [ + { + "type": "text", + "text": "npx @tiptap/cli init" + } + ] + }, + { + "type": "heading", + "attrs": { + "textAlign": null, + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Features" + } + ] + }, + { + "type": "blockquote", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": null + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "A fully responsive rich text editor with built-in support for common formatting and layout tools. Type markdown " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "**" + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": " or use keyboard shortcuts " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "⌘+B" + }, + { + "type": "text", + "text": " for " + }, + { + "type": "text", + "marks": [ + { + "type": "strike" + } + ], + "text": "most" + }, + { + "type": "text", + "text": " all common markdown marks. 🪄" + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Add images, customize alignment, and apply " + }, + { + "type": "text", + "marks": [ + { + "type": "highlight", + "attrs": { + "color": "var(--tt-color-highlight-blue)" + } + } + ], + "text": "advanced formatting" + }, + { + "type": "text", + "text": " to make your writing more engaging and professional." + } + ] + }, + { + "type": "image", + "attrs": { + "src": "/images/tiptap-ui-placeholder-image.jpg", + "alt": "placeholder-image", + "title": "placeholder-image" + } + }, + { + "type": "bulletList", + "content": [ + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Superscript" + }, + { + "type": "text", + "text": " (x" + }, + { + "type": "text", + "marks": [ + { + "type": "superscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": ") and " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Subscript" + }, + { + "type": "text", + "text": " (H" + }, + { + "type": "text", + "marks": [ + { + "type": "subscript" + } + ], + "text": "2" + }, + { + "type": "text", + "text": "O) for precision." + } + ] + } + ] + }, + { + "type": "listItem", + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "Typographic conversion" + }, + { + "type": "text", + "text": ": automatically convert to " + }, + { + "type": "text", + "marks": [ + { + "type": "code" + } + ], + "text": "->" + }, + { + "type": "text", + "text": " an arrow " + }, + { + "type": "text", + "marks": [ + { + "type": "bold" + } + ], + "text": "→" + }, + { + "type": "text", + "text": "." + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "→ " + }, + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor#features", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Learn more" + } + ] + }, + { + "type": "horizontalRule" + }, + { + "type": "heading", + "attrs": { + "textAlign": "left", + "level": 2 + }, + "content": [ + { + "type": "text", + "text": "Make it your own" + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Switch between light and dark modes, and tailor the editor's appearance with customizable CSS to match your style." + } + ] + }, + { + "type": "taskList", + "content": [ + { + "type": "taskItem", + "attrs": { + "checked": true + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "text": "Test template" + } + ] + } + ] + }, + { + "type": "taskItem", + "attrs": { + "checked": false + }, + "content": [ + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + }, + "content": [ + { + "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "https://tiptap.dev/docs/ui-components/templates/simple-editor", + "target": "_blank", + "rel": "noopener noreferrer nofollow", + "class": null + } + } + ], + "text": "Integrate the free template" + } + ] + } + ] + } + ] + }, + { + "type": "paragraph", + "attrs": { + "textAlign": "left" + } + } + ] +} \ No newline at end of file