From ab9fdfe8564ebb0f23e5e6cbf22317e5d7425074 Mon Sep 17 00:00:00 2001 From: zhongqin Date: Wed, 20 May 2026 15:43:27 +0800 Subject: [PATCH] unified refactoring id, using snowflake algorithm id --- .cargo/config.toml | 2 + .gitignore | 1 + ...7bdc1127ad17048bce48ea53c3fb13311d202.json | 12 + ...08e4c4d2c1c45f2bbffa52e54ac4a34f96519.json | 12 + ...03778459a13f196e241481b5ff2289c9861a1.json | 12 + ...07b94ab60a33891316c437e88f8b2ac3aef5d.json | 12 + ...9cc8961ba7f5f7ab3ca6bf011446d48cc9c11.json | 12 - ...52efe7faed0cbd129af7dd34d3f6679a0498d.json | 12 + ...d59d31c4feade2e729ab5934ad2df2709fb34.json | 12 + ...bb3045c8ef74f270ff32d1d7010550233b71f.json | 12 + ...b4996d3782dd360c6f5cd5a24b5775335c44a.json | 12 + ...119777c9bd76a94b971019c5f09a1b639b211.json | 12 + ...bf23024dde72e97b064426ad3072f00ba0133.json | 12 + ...385e000e53257c840a1a0f8ed245e296c0895.json | 12 - ...eade3f7c99a9c6094248ef22a3ae204bb789b.json | 12 - ...e9e03e36c853824a2ca7697f890d6a6a37b2b.json | 12 - ...c76ab58184487ffc5f4145c76210a0828e09a.json | 12 - ...6df8ba82f145544e6c3f34a236782ff0f36f8.json | 12 - ...ba41a6df71a318dcfdd3258827eb1d1b04f6e.json | 12 + ...d8035746e68df2bfabc61d71382f92785f7cb.json | 12 - ...55d13787e0d15c492dffe8b9e3449217e4290.json | 12 + ...9643f2e4b71650f2b1291ac7f106448e9ffa5.json | 12 - ...8a30d832371735dc44f776bbffc9af6c27034.json | 12 - ...35551d5e7ff56e29cfb0bd7122f4909bf7020.json | 12 - ...2a43e65fc8e54ac0e31dc25876ed11e6a75c6.json | 12 + ...b0a0d8a13ba6df8a3538f2351143866e46afd.json | 12 - ...dcfdaf596ff1b172b29d02008f38ec680befc.json | 12 + ...b9f3b75ed64146de924cd5410374b6e560ffb.json | 12 + ...25522d8a6d4ae3cd90d12955d9083b4532ca9.json | 12 + ...d01d638cce4e957266188c7828dc83b9c1c95.json | 12 + ...631d370f2c20ca7e76b43f226a4dd09c130f0.json | 12 + ...2122a3df4e65b776327b6d8ae61105f680dab.json | 12 + ...c821eaa411367d12e490abea5f787d927d765.json | 12 - ...00a362791b56b0022249e60293f8f8f766b6e.json | 12 - ...bd58962be98f0436de25401971c037f49f497.json | 12 - ...069931238caa8aa1a77275217a01142061324.json | 12 - ...4cd4a4b6d9846effe43beec8f7d075fac6976.json | 12 - ...7f15513e3372366172ef010cf08e498994c8b.json | 12 + ...83e01d7b28e92c4b26f1aa45af22fba412837.json | 12 - ...7e71b001c9600d343da1e3fb005aa6849e55a.json | 12 - ...637dfa6d697206896e963abd2281ba910619a.json | 12 + ...1082a3459e408b4d3e9d7c1f4dd6408897010.json | 12 + ...2e4449df61bb999e90876e89efe1c0d78a4f0.json | 12 - ...f70bf9638cc9dd2543eef84f3c81b74f80542.json | 12 - ...1a24e6834a0da52952a5b2f21221681886d7d.json | 12 + ...9160013287aa0cbbab8c5204b5dbe3e54a3c3.json | 12 + ...afc1d5ff18fd66510b25cabccbcc581c76e30.json | 12 + ...c0d35586cd07a24fd6232df173e3ffc2b69f7.json | 12 + ...f273da0d49943754ebf8e715988cea21fe3ae.json | 12 + ...0b32a67eedf844d63c3192a1134bcc84d780b.json | 12 + ...c79b6f86ab3f05585608732ff011c65a01a48.json | 12 - ...5fad8e863f216e92257722c2a271ba1d8dcf3.json | 12 - ...cb605e246efe53cc29338388d8728fa432e57.json | 12 + ...1b3c9a7d123b5172b5bb416f254dc6baa4417.json | 12 - ...965de18bbfe6b0b3e4b355da425111ee861b5.json | 12 - ...2aa2b75302feb99d520fdef4bb3a709d28379.json | 12 + ...8c299a7dd62527ae871f137511d88d7619618.json | 12 - ...204ec02d5abccf54481ef54e68eccf7ae11f9.json | 12 + ...1110954448b5ec490745547285373928ecc2f.json | 12 - ...dfbedbb382e5cebe261fd3267980d21c4d5b4.json | 12 + ...073700bd7ac970f3280f861401fcc272f8dbf.json | 12 - ...1535580c664613b491756da16620aef9779de.json | 12 - ...eb87fd7ead53fd670cd0ba7bd94c51ba23ac9.json | 12 - ...72b504edf1ea8c4d20a60117b4c0a37e4ba33.json | 12 + ...2e9f57fb5fc74753c00b0f311a625e5fc2200.json | 12 - ...6f20b2beb890624abac1f68cdb23755dd1373.json | 12 - ...0487a9210946793a8a41d6e2e7b8536c59559.json | 12 + ...41dc77b26b68ca0ab101df4cf3a7ab21fe502.json | 12 + ...7505b8fd06507bdc16a31aaabdc21cbc33c5c.json | 12 + ...c40618bfa67fede2ebb5d5863319691147250.json | 12 - ...5e22d8585d2550bb5ea22761294cc57432d9f.json | 12 + ...68833de5aec68c78727c675486613e5671889.json | 12 - ...c55c49d1b9fc4c7af6e2f8ca3b198a9341014.json | 12 - ...7dbd1c75d573ea8502e2b748cacdd3fd9bec7.json | 12 - ...acdef534b7bed029494180df79eeb01d11b86.json | 12 + ...464e5e34b117c8c1cbd319ddf6a271a928839.json | 12 - ...84a074af56e5bb902c0144b68a2fa3caa6fd5.json | 12 - ...bc8fe7621e783dc86af4af9ccaccf2263b0b3.json | 12 + ...8ff38ae754f197a493183d29003daf53c1912.json | 12 + ...235a7a0e04bce6209e288ceb8d753eb8a3773.json | 12 - ...6c12935fc8e3650bbd6aa394a3564ce07076f.json | 12 - ...c747cb9685f805763b8628f8a7433bddbd17e.json | 12 + ...9563530efe3a3762bc04d8393e0302f5d1741.json | 12 - ...1272644c6d0521b0217d32199150c7b5d84be.json | 12 + ...8f9c9b3e9d8b37e8db0e0bacc44ec2aea944a.json | 12 - ...8ce24d1b84cb148c87fdcbda5f69e9c267743.json | 12 + ...aeecaef1b744ac6ad762fd8f95fca78a076be.json | 12 - ...282090c0b874557bf646ef9b2e7c623c28cde.json | 12 + ...c8adb077ae16fc31ce2f9c972f679593eac3d.json | 12 + ...9b0ec18617974eea17f926a4ea4064549854f.json | 12 + ...2088b03fbdb7843bd7868c4cfcc0381112922.json | 12 - ...5d470924d68eccf4acfe0ca71907d46f7ea87.json | 12 + ...540af18caa9d9f78e788054056e1c4ef939af.json | 12 + ...db7b56298aecfe3f00ec1c3731ed65bd4a8fa.json | 12 - ...b21a2a87033ee69ee5a5e9498062e4e49722c.json | 12 + ...b612494c1374783989d5a1353a99cb39cbe90.json | 12 + ...cda1cff0a844e474bc8e2df67ec60d7c8c9a9.json | 12 + ...abb752dec62b87556f0084b8a7d826b9a32c4.json | 12 + ...44b3a1e1617da56f9929ba3a1b05e2d9b0815.json | 12 - ...3f80ef1145b31d56d43bb780b8db500f4c6c2.json | 12 + ...258aabd4f0688856614fceb7eead0a17114ce.json | 12 + ...044af92977a83f9f5b5adccd2c8b163926900.json | 12 + ...6be599279b1adc3978387c42eca5eb7fd23ad.json | 12 + ...fa9553d710aaeccc27b0bf2c52e9e9fc17bbd.json | 12 - ...32a1d629cb5e63931b2ef9a98739a71eab951.json | 12 - ...8bff5ce0c60e5aaf4be088a21dad6b952d7be.json | 12 - ...04fd9b32b3fdfdbee25f64583e14ae20aad60.json | 12 - ...a91fa2fd60313ffd0a56312b0bb3b176d5f27.json | 12 - ...28ab19e1242864d21955ba1878a14ab6ea5ae.json | 12 - ...5657927befc68b0a5f4a3c5064e7e356500bb.json | 12 - ...5c4d34c7772b2c1455ec1ac6f22073d28797d.json | 12 - ...6f2ff4a9e570df4ca962178674b8bda40072c.json | 12 - ...3c4ad73923e8d73eb6af6f1bbe67df7c837bb.json | 12 - ...2af772028c349bd46e073b15a6055364b059f.json | 12 + ...f06900f9650c01673379f6eb26e0b42df1487.json | 12 + ...0069bcfbfe61ea8befde895bae56980e4f778.json | 12 + ...d49e37541daf536c2ce5b13d37bf7594dd208.json | 12 + ...0d88f603d86cc04cb4b55c61192fa057dbb94.json | 12 - ...ba8e25efcc061146be913ea01bc7b1dd6b1f1.json | 12 - ...0ec40bb2f1cce89e3da14702bb25700cd6688.json | 12 - ...028846c2a16e8be98006ec369bdd3e0ddf015.json | 12 - ...e429c06452429f234660b1e85e6cc38fc51aa.json | 12 + ...4d856f0871a0f8a8dd2c5e8a17b53f8b8c867.json | 12 - ...2b0b0b8b978b47085031789ad5a742cfcc4ba.json | 12 - ...92235453e78aea05731513fbe11f54dede031.json | 12 - ...e796a81107e90c4c38aea79b5f71102713cde.json | 12 - ...19ba207e7da6c97254a23c2f5e2773ff32a13.json | 12 + ...10edf13203ae3dabc6053f4b8dd9047417c17.json | 12 + ...2d98b16d21f71ce69f379a9dc74bf85354f52.json | 12 + ...dc7078d390a041bb999e757b1cb7e78dc9de8.json | 12 - ...54d76551d25a16eebec367d92f5ee987d2ade.json | 12 + ...d0c83f4e7f42e239c67fb2780a2791beb849d.json | 12 + ...a832f1fb464ae29bac5f0623b4f1766b719c2.json | 12 - ...b3062128639546a2e1bac4d8f6fccf6e16bc1.json | 12 - Cargo.lock | 104 ++++- Cargo.toml | 2 + frontend/sdk/src/generated/types.ts | 408 ++++------------- justfile | 2 +- migrations/mysql/schema.mysql.sql | 113 ++--- migrations/postgres/schema.postgres.sql | 127 ++---- migrations/sqlite/schema.sqlite.sql | 113 ++--- src/aspects/engine.rs | 3 +- src/bin/export-types.rs | 4 +- src/cli.rs | 32 ++ src/cli/codegen_cmd.rs | 278 ++++++++++++ src/cli/db_cmd.rs | 20 +- src/commands/page.rs | 1 + src/constants.rs | 2 - src/content_type/handler.rs | 136 +++--- src/content_type/migration.rs | 23 +- src/content_type/repository.rs | 220 +++++----- src/content_type/resolver.rs | 71 +-- src/content_type/rule_engine.rs | 34 +- src/content_type/schema.rs | 7 - src/content_type/validation.rs | 30 +- src/db/tenant.rs | 94 ++-- src/dto.rs | 6 + src/dto/cart.rs | 1 - src/dto/category.rs | 29 ++ src/dto/currencies.rs | 2 +- src/dto/ecommerce.rs | 6 +- src/dto/media.rs | 7 +- src/dto/order.rs | 11 +- src/dto/page.rs | 39 ++ src/dto/payment.rs | 20 +- src/dto/post.rs | 10 +- src/dto/reusable_block.rs | 28 ++ src/dto/tag.rs | 24 + src/dto/tenant.rs | 32 ++ src/dto/user.rs | 6 +- src/dto/wallet.rs | 8 +- src/event/mod.rs | 32 +- src/eventbus.rs | 10 +- src/graphql/mutation.rs | 36 +- src/graphql/query.rs | 6 +- src/handlers/cart.rs | 21 +- src/handlers/category.rs | 58 ++- src/handlers/comment.rs | 17 +- src/handlers/content_revision.rs | 39 +- src/handlers/cron.rs | 21 +- src/handlers/order.rs | 38 +- src/handlers/page.rs | 56 ++- src/handlers/payment.rs | 58 +-- src/handlers/post.rs | 6 +- src/handlers/product.rs | 9 +- src/handlers/product_variant.rs | 9 +- src/handlers/rbac.rs | 11 +- src/handlers/reusable_block.rs | 44 +- src/handlers/sse.rs | 30 +- src/handlers/tag.rs | 49 ++- src/handlers/tenant.rs | 32 +- src/handlers/user.rs | 6 +- src/handlers/user_address.rs | 8 +- src/handlers/wallet.rs | 42 +- src/middleware/auth.rs | 122 ++---- src/models/api_token.rs | 45 +- src/models/audit_log.rs | 16 +- src/models/cart_item.rs | 90 ++-- src/models/category.rs | 52 +-- src/models/comment.rs | 165 +++---- src/models/content_revision.rs | 31 +- src/models/currencies.rs | 18 +- src/models/email_verification.rs | 16 +- src/models/media.rs | 21 +- src/models/oauth.rs | 80 ++-- src/models/options.rs | 30 +- src/models/order.rs | 211 +++------ src/models/order_item.rs | 39 +- src/models/page.rs | 57 +-- src/models/password_reset.rs | 20 +- src/models/payment_channel.rs | 62 +-- src/models/payment_order.rs | 83 ++-- src/models/payment_refund.rs | 46 +- src/models/payment_transaction.rs | 52 +-- src/models/post.rs | 137 +++--- src/models/product.rs | 80 ++-- src/models/product_variant.rs | 64 +-- src/models/rbac.rs | 87 ++-- src/models/refresh_token.rs | 24 +- src/models/reusable_block.rs | 58 +-- src/models/sms_code.rs | 26 +- src/models/tag.rs | 53 +-- src/models/tenant.rs | 75 ++-- src/models/user.rs | 57 +-- src/models/user_address.rs | 73 +--- src/models/user_credential.rs | 13 +- src/models/wallet.rs | 53 ++- src/models/wallet_outbox.rs | 10 +- src/models/wallet_transaction.rs | 72 +-- src/payment/providers/alipay.rs | 5 +- src/payment/providers/creem.rs | 9 +- src/payment/providers/dodo.rs | 8 +- src/payment/providers/stripe.rs | 2 +- src/payment/providers/wechat.rs | 6 +- src/payment/routing.rs | 3 +- src/plugins.rs | 20 +- src/plugins/host_common.rs | 213 +++------ src/policy.rs | 40 +- src/protocols.rs | 4 +- src/protocols/versionable.rs | 6 +- src/server.rs | 32 +- src/services/api_token.rs | 122 +++--- src/services/audit.rs | 7 +- src/services/auth.rs | 100 ++--- src/services/cart.rs | 60 ++- src/services/category.rs | 67 ++- src/services/comment.rs | 109 ++--- src/services/content_revision.rs | 8 +- src/services/email_verification.rs | 16 +- src/services/media.rs | 10 +- src/services/oauth.rs | 56 +-- src/services/order.rs | 279 ++++++------ src/services/page.rs | 57 +-- src/services/password_reset.rs | 30 +- src/services/payment.rs | 411 ++++++++---------- src/services/post.rs | 155 +++---- src/services/product.rs | 51 +-- src/services/product_variant.rs | 74 ++-- src/services/rbac.rs | 33 +- src/services/reusable_block.rs | 20 +- src/services/sms.rs | 15 +- src/services/stats.rs | 40 +- src/services/tag.rs | 46 +- src/services/tenant.rs | 43 +- src/services/user.rs | 74 ++-- src/services/user_address.rs | 34 +- src/services/wallet.rs | 325 +++++++------- src/tauri/commands.rs | 14 +- src/utils/id.rs | 182 ++++++-- src/webhook/handler.rs | 21 +- src/webhook/model.rs | 27 +- src/webhook/service.rs | 98 +---- src/worker.rs | 4 +- src/worker/enqueuer.rs | 25 +- src/worker/handlers/payment_expire.rs | 12 +- src/worker/handlers/payment_reconcile.rs | 12 +- src/worker/handlers/payment_retry.rs | 34 +- src/worker/handlers/publish.rs | 21 +- src/worker/handlers/search_index.rs | 24 +- src/worker/handlers/sitemap.rs | 7 +- src/worker/handlers/wallet_outbox.rs | 14 +- src/worker/runner.rs | 17 +- src/worker/scheduler.rs | 196 ++++----- src/worker/sqlite_queue.rs | 103 +++-- src/workflow/engine.rs | 336 +++++++------- src/workflow/handler.rs | 49 +-- src/workflow/model.rs | 251 +++++------ templates/codegen/model.rs.tera | 80 ++++ tests/api.rs | 48 +- tests/api/api_token.rs | 66 +-- tests/api/audit.rs | 4 +- tests/api/cart.rs | 6 +- tests/api/category.rs | 8 +- tests/api/comment.rs | 30 +- tests/api/cron.rs | 40 +- tests/api/media.rs | 12 +- tests/api/order.rs | 8 +- tests/api/page.rs | 20 +- tests/api/payment.rs | 27 +- tests/api/plugin.rs | 4 +- tests/api/post.rs | 40 +- tests/api/product.rs | 6 +- tests/api/product_variant.rs | 4 +- tests/api/rbac.rs | 16 +- tests/api/reusable_block.rs | 16 +- tests/api/rss.rs | 4 +- tests/api/tag.rs | 8 +- tests/api/tenant_admin.rs | 16 +- tests/api/tenant_e2e.rs | 106 ++--- tests/api/user.rs | 27 +- tests/api/wallet.rs | 48 +- tests/api/webhook.rs | 25 +- tests/api/workflow.rs | 85 ++-- tests/content_type_tests.rs | 171 +++++--- tests/tauri_tests.rs | 38 +- 315 files changed, 5384 insertions(+), 6032 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 .sqlx/query-002f8f9b95de626bf0382c30ae57bdc1127ad17048bce48ea53c3fb13311d202.json create mode 100644 .sqlx/query-00f6bad9968219ec438c35106c208e4c4d2c1c45f2bbffa52e54ac4a34f96519.json create mode 100644 .sqlx/query-0218fef502ae157fa71b54e879f03778459a13f196e241481b5ff2289c9861a1.json create mode 100644 .sqlx/query-05732b48dbf7a440862fda2158c07b94ab60a33891316c437e88f8b2ac3aef5d.json delete mode 100644 .sqlx/query-05f14ae8bf8557a31721d064cb39cc8961ba7f5f7ab3ca6bf011446d48cc9c11.json create mode 100644 .sqlx/query-06cc780f1876253554647fef3b852efe7faed0cbd129af7dd34d3f6679a0498d.json create mode 100644 .sqlx/query-0745f93cf3ba9ee9a8763e8f609d59d31c4feade2e729ab5934ad2df2709fb34.json create mode 100644 .sqlx/query-090094cc844e65a54240b25e553bb3045c8ef74f270ff32d1d7010550233b71f.json create mode 100644 .sqlx/query-09a8ac8bc87bb8f221593dcbb39b4996d3782dd360c6f5cd5a24b5775335c44a.json create mode 100644 .sqlx/query-0d9cf697203f12beace57613de3119777c9bd76a94b971019c5f09a1b639b211.json create mode 100644 .sqlx/query-0f7df02f4e4e2d810a2fec17a14bf23024dde72e97b064426ad3072f00ba0133.json delete mode 100644 .sqlx/query-0ffb5489ffefc25cfc4e75e33b1385e000e53257c840a1a0f8ed245e296c0895.json delete mode 100644 .sqlx/query-10ec8354fb857f2524cf7f8f56deade3f7c99a9c6094248ef22a3ae204bb789b.json delete mode 100644 .sqlx/query-118ed2067882c9119430926e6c1e9e03e36c853824a2ca7697f890d6a6a37b2b.json delete mode 100644 .sqlx/query-14bfe946a89dfafed01d3a351e0c76ab58184487ffc5f4145c76210a0828e09a.json delete mode 100644 .sqlx/query-1891058a8e02bf3890a7fb79e156df8ba82f145544e6c3f34a236782ff0f36f8.json create mode 100644 .sqlx/query-1a6350d43916712692fd0b7abc6ba41a6df71a318dcfdd3258827eb1d1b04f6e.json delete mode 100644 .sqlx/query-1a66e50cdcdce85103ef5a84e93d8035746e68df2bfabc61d71382f92785f7cb.json create mode 100644 .sqlx/query-1a804863d5148834f4290bb2de855d13787e0d15c492dffe8b9e3449217e4290.json delete mode 100644 .sqlx/query-1f0c8777464de4aa46699f24fd19643f2e4b71650f2b1291ac7f106448e9ffa5.json delete mode 100644 .sqlx/query-2484b7be5ea6d715276744baeda8a30d832371735dc44f776bbffc9af6c27034.json delete mode 100644 .sqlx/query-2550524dc2d2b8188b8845dc2af35551d5e7ff56e29cfb0bd7122f4909bf7020.json create mode 100644 .sqlx/query-269bfe69eadc1f77125ede936102a43e65fc8e54ac0e31dc25876ed11e6a75c6.json delete mode 100644 .sqlx/query-276f1d700d4f7f1964fd3750cb4b0a0d8a13ba6df8a3538f2351143866e46afd.json create mode 100644 .sqlx/query-28a6772da782aeab44de5b1ab38dcfdaf596ff1b172b29d02008f38ec680befc.json create mode 100644 .sqlx/query-2919b493c38a046808ace3dfc0bb9f3b75ed64146de924cd5410374b6e560ffb.json create mode 100644 .sqlx/query-295429ee6c6e6a6a53e2942286825522d8a6d4ae3cd90d12955d9083b4532ca9.json create mode 100644 .sqlx/query-29ec564f478f99e306a62409f06d01d638cce4e957266188c7828dc83b9c1c95.json create mode 100644 .sqlx/query-2a99974dd884c439c750b857f54631d370f2c20ca7e76b43f226a4dd09c130f0.json create mode 100644 .sqlx/query-347ea867704d076b7cc725da4bf2122a3df4e65b776327b6d8ae61105f680dab.json delete mode 100644 .sqlx/query-3a7ddd71e735af46194a8d65fe4c821eaa411367d12e490abea5f787d927d765.json delete mode 100644 .sqlx/query-3b5a2cbe347a7580d9a86a6397700a362791b56b0022249e60293f8f8f766b6e.json delete mode 100644 .sqlx/query-3ec645d617cb339c01ed71f621ebd58962be98f0436de25401971c037f49f497.json delete mode 100644 .sqlx/query-3eca34da3315899e2c42270e042069931238caa8aa1a77275217a01142061324.json delete mode 100644 .sqlx/query-3f5e106e8664eac53a52b59f3724cd4a4b6d9846effe43beec8f7d075fac6976.json create mode 100644 .sqlx/query-410b34079c9d2e443b87e0b5b0c7f15513e3372366172ef010cf08e498994c8b.json delete mode 100644 .sqlx/query-42102721fa299de81560fb549e283e01d7b28e92c4b26f1aa45af22fba412837.json delete mode 100644 .sqlx/query-48321ccbd123a9ffccc9ebbda3c7e71b001c9600d343da1e3fb005aa6849e55a.json create mode 100644 .sqlx/query-4b747395c48682c15da802e576e637dfa6d697206896e963abd2281ba910619a.json create mode 100644 .sqlx/query-4d173dc00623dbb15d982fa725d1082a3459e408b4d3e9d7c1f4dd6408897010.json delete mode 100644 .sqlx/query-4e49d760f473b689691830c73f52e4449df61bb999e90876e89efe1c0d78a4f0.json delete mode 100644 .sqlx/query-5295a282c7156cc458c9c8261caf70bf9638cc9dd2543eef84f3c81b74f80542.json create mode 100644 .sqlx/query-5758dc7c2634b3a00cf929c79df1a24e6834a0da52952a5b2f21221681886d7d.json create mode 100644 .sqlx/query-57c76d4195d5a4778ffdd1374bd9160013287aa0cbbab8c5204b5dbe3e54a3c3.json create mode 100644 .sqlx/query-5a101242e9a58a8e8c028579d8fafc1d5ff18fd66510b25cabccbcc581c76e30.json create mode 100644 .sqlx/query-5ce3f0fefb05bf54aeca419eda8c0d35586cd07a24fd6232df173e3ffc2b69f7.json create mode 100644 .sqlx/query-60dc717206e2519ccacbbb8c4e9f273da0d49943754ebf8e715988cea21fe3ae.json create mode 100644 .sqlx/query-619bbdf9be92fbc7252df50e6900b32a67eedf844d63c3192a1134bcc84d780b.json delete mode 100644 .sqlx/query-651405c96c6d0ca6ab5e56fb4c8c79b6f86ab3f05585608732ff011c65a01a48.json delete mode 100644 .sqlx/query-69ac6a75bbfdd641366092f22e55fad8e863f216e92257722c2a271ba1d8dcf3.json create mode 100644 .sqlx/query-6b15cd2e3955588956ba22aa093cb605e246efe53cc29338388d8728fa432e57.json delete mode 100644 .sqlx/query-6e0ec4602996acf7060db60846f1b3c9a7d123b5172b5bb416f254dc6baa4417.json delete mode 100644 .sqlx/query-6e1f6b059bf443b532ec684286a965de18bbfe6b0b3e4b355da425111ee861b5.json create mode 100644 .sqlx/query-7015463e67a587c677bf5f192ba2aa2b75302feb99d520fdef4bb3a709d28379.json delete mode 100644 .sqlx/query-701ef74f1650e40f6c128b1a8978c299a7dd62527ae871f137511d88d7619618.json create mode 100644 .sqlx/query-7051eadf2365278e99621914872204ec02d5abccf54481ef54e68eccf7ae11f9.json delete mode 100644 .sqlx/query-75a5da6ef797a9c43bb377cd90c1110954448b5ec490745547285373928ecc2f.json create mode 100644 .sqlx/query-779500a155e4d67dd7cf63a53e7dfbedbb382e5cebe261fd3267980d21c4d5b4.json delete mode 100644 .sqlx/query-7943383ac8989afd5bc1b279fb2073700bd7ac970f3280f861401fcc272f8dbf.json delete mode 100644 .sqlx/query-79ad88673cd2323868f8f5d920d1535580c664613b491756da16620aef9779de.json delete mode 100644 .sqlx/query-7d4b62eba6f6bd5abf49894fb3feb87fd7ead53fd670cd0ba7bd94c51ba23ac9.json create mode 100644 .sqlx/query-7e106ee0128fc4597f89a594f3f72b504edf1ea8c4d20a60117b4c0a37e4ba33.json delete mode 100644 .sqlx/query-802f6376046b4a50fc157f3cb132e9f57fb5fc74753c00b0f311a625e5fc2200.json delete mode 100644 .sqlx/query-80d009ab24ccedf6aa87d6d5d166f20b2beb890624abac1f68cdb23755dd1373.json create mode 100644 .sqlx/query-816d3e64cf350633d70c14241f20487a9210946793a8a41d6e2e7b8536c59559.json create mode 100644 .sqlx/query-85ddf6296df989403dd05ce8fce41dc77b26b68ca0ab101df4cf3a7ab21fe502.json create mode 100644 .sqlx/query-8b4690ba53bdc8815dec7ce06c77505b8fd06507bdc16a31aaabdc21cbc33c5c.json delete mode 100644 .sqlx/query-8d0d83f16e4ad0c7240ebf94971c40618bfa67fede2ebb5d5863319691147250.json create mode 100644 .sqlx/query-8f84d596eb12222a16f3137fb2f5e22d8585d2550bb5ea22761294cc57432d9f.json delete mode 100644 .sqlx/query-8fa80a24d1be10f16c3ab45822768833de5aec68c78727c675486613e5671889.json delete mode 100644 .sqlx/query-927486c9db9989a3187eea43ca6c55c49d1b9fc4c7af6e2f8ca3b198a9341014.json delete mode 100644 .sqlx/query-96d2694f4b75de232afa30f0f9f7dbd1c75d573ea8502e2b748cacdd3fd9bec7.json create mode 100644 .sqlx/query-97829829cd0cb4cba104522aaadacdef534b7bed029494180df79eeb01d11b86.json delete mode 100644 .sqlx/query-9916f8547bf3da10e652e12e4b1464e5e34b117c8c1cbd319ddf6a271a928839.json delete mode 100644 .sqlx/query-994ed64b3beb28d532fb3ea1d1c84a074af56e5bb902c0144b68a2fa3caa6fd5.json create mode 100644 .sqlx/query-9b99a14fd1f97d84704a85c0ecabc8fe7621e783dc86af4af9ccaccf2263b0b3.json create mode 100644 .sqlx/query-9f2b0f4b8dd82f81ffc0802eca98ff38ae754f197a493183d29003daf53c1912.json delete mode 100644 .sqlx/query-a22b7f5f04d6a41911cec171831235a7a0e04bce6209e288ceb8d753eb8a3773.json delete mode 100644 .sqlx/query-a499c71b4a7b0936e4661b4bcbd6c12935fc8e3650bbd6aa394a3564ce07076f.json create mode 100644 .sqlx/query-a59c89fc04a16acca91715ef62fc747cb9685f805763b8628f8a7433bddbd17e.json delete mode 100644 .sqlx/query-a9127f524b96e405be2128d8bcd9563530efe3a3762bc04d8393e0302f5d1741.json create mode 100644 .sqlx/query-a9f3579fc327fad0377051402d61272644c6d0521b0217d32199150c7b5d84be.json delete mode 100644 .sqlx/query-acdcafc1ae4bf2bc137e9c0b8468f9c9b3e9d8b37e8db0e0bacc44ec2aea944a.json create mode 100644 .sqlx/query-b48d717f1f3713fcc2d11baa83d8ce24d1b84cb148c87fdcbda5f69e9c267743.json delete mode 100644 .sqlx/query-b69d3a2b6e56a6a2b54664758beaeecaef1b744ac6ad762fd8f95fca78a076be.json create mode 100644 .sqlx/query-b76f4e37b51afe15519f57c4046282090c0b874557bf646ef9b2e7c623c28cde.json create mode 100644 .sqlx/query-b8551631f34baaa46ef4ac31286c8adb077ae16fc31ce2f9c972f679593eac3d.json create mode 100644 .sqlx/query-b8c1052e2e35b3c762d36e1123d9b0ec18617974eea17f926a4ea4064549854f.json delete mode 100644 .sqlx/query-b8c239c447aaf669ea6c83ddf842088b03fbdb7843bd7868c4cfcc0381112922.json create mode 100644 .sqlx/query-b972949428e68d37ad3075ff6c15d470924d68eccf4acfe0ca71907d46f7ea87.json create mode 100644 .sqlx/query-b9a06aa2f8b4e7fd69470ffc826540af18caa9d9f78e788054056e1c4ef939af.json delete mode 100644 .sqlx/query-bb350274e9cf006caa1d3f0f8d1db7b56298aecfe3f00ec1c3731ed65bd4a8fa.json create mode 100644 .sqlx/query-bccaa81b9d8979c9313a585be1ab21a2a87033ee69ee5a5e9498062e4e49722c.json create mode 100644 .sqlx/query-be4324ba43c80e343b654c60768b612494c1374783989d5a1353a99cb39cbe90.json create mode 100644 .sqlx/query-bed615f4573501020fa03300df7cda1cff0a844e474bc8e2df67ec60d7c8c9a9.json create mode 100644 .sqlx/query-c14c06e841097b2f930e247ef56abb752dec62b87556f0084b8a7d826b9a32c4.json delete mode 100644 .sqlx/query-c60e6d14f4fbfb5552cd51de32444b3a1e1617da56f9929ba3a1b05e2d9b0815.json create mode 100644 .sqlx/query-ca026c702e1f298987fddaeef413f80ef1145b31d56d43bb780b8db500f4c6c2.json create mode 100644 .sqlx/query-ccf6d9edfc09aab5a4e082468c3258aabd4f0688856614fceb7eead0a17114ce.json create mode 100644 .sqlx/query-cd430e82ffb58f8831d5720503a044af92977a83f9f5b5adccd2c8b163926900.json create mode 100644 .sqlx/query-cdaf7a8cb2657d0c7cd05d9219f6be599279b1adc3978387c42eca5eb7fd23ad.json delete mode 100644 .sqlx/query-ce75e804f68650c3f9eb766a5c8fa9553d710aaeccc27b0bf2c52e9e9fc17bbd.json delete mode 100644 .sqlx/query-cff66d74ea51fad9a845b398e6832a1d629cb5e63931b2ef9a98739a71eab951.json delete mode 100644 .sqlx/query-d0f119cc1830f2196da20e43ad68bff5ce0c60e5aaf4be088a21dad6b952d7be.json delete mode 100644 .sqlx/query-d3efddaf5242931a09636e71de804fd9b32b3fdfdbee25f64583e14ae20aad60.json delete mode 100644 .sqlx/query-d887b374016efd58de5881a05c9a91fa2fd60313ffd0a56312b0bb3b176d5f27.json delete mode 100644 .sqlx/query-db043f4ef370f2636f8ac7441f328ab19e1242864d21955ba1878a14ab6ea5ae.json delete mode 100644 .sqlx/query-dc13649fc9396997f38902632b95657927befc68b0a5f4a3c5064e7e356500bb.json delete mode 100644 .sqlx/query-dc51c24ab845e4bfac57e5eb1865c4d34c7772b2c1455ec1ac6f22073d28797d.json delete mode 100644 .sqlx/query-e09f07a3f57f2417cec71e94f856f2ff4a9e570df4ca962178674b8bda40072c.json delete mode 100644 .sqlx/query-e1764e3c39ab4ce487084f4129d3c4ad73923e8d73eb6af6f1bbe67df7c837bb.json create mode 100644 .sqlx/query-e35f9b20d7340b4c59889732d552af772028c349bd46e073b15a6055364b059f.json create mode 100644 .sqlx/query-e38cf91855fc4c311b74f695bf6f06900f9650c01673379f6eb26e0b42df1487.json create mode 100644 .sqlx/query-e42e6f66a9e9eaf49e3d2b554ed0069bcfbfe61ea8befde895bae56980e4f778.json create mode 100644 .sqlx/query-e4edc339cde1b12bdbe5e8cc761d49e37541daf536c2ce5b13d37bf7594dd208.json delete mode 100644 .sqlx/query-e7ec039830a9766566351f7ca540d88f603d86cc04cb4b55c61192fa057dbb94.json delete mode 100644 .sqlx/query-e881e1880fced15594a1004a1b5ba8e25efcc061146be913ea01bc7b1dd6b1f1.json delete mode 100644 .sqlx/query-eb303f6bfba8d1831c84691aba50ec40bb2f1cce89e3da14702bb25700cd6688.json delete mode 100644 .sqlx/query-f2afec774a9737e8fccc17f3470028846c2a16e8be98006ec369bdd3e0ddf015.json create mode 100644 .sqlx/query-f3be8b5cb0b4143655c4b57479ce429c06452429f234660b1e85e6cc38fc51aa.json delete mode 100644 .sqlx/query-f52733df58c6572f8a76d0119bb4d856f0871a0f8a8dd2c5e8a17b53f8b8c867.json delete mode 100644 .sqlx/query-f5d65300def492fc78e23578e7e2b0b0b8b978b47085031789ad5a742cfcc4ba.json delete mode 100644 .sqlx/query-f6843d1c158437e04709ff74f1c92235453e78aea05731513fbe11f54dede031.json delete mode 100644 .sqlx/query-f758e4d5a6991e6e974b2e7a6bce796a81107e90c4c38aea79b5f71102713cde.json create mode 100644 .sqlx/query-f8207c8b05da03ca3f2c792ba7019ba207e7da6c97254a23c2f5e2773ff32a13.json create mode 100644 .sqlx/query-f9f73cc5a5be6b4c2c69d0250ad10edf13203ae3dabc6053f4b8dd9047417c17.json create mode 100644 .sqlx/query-f9fcacf81191ee798e000cd9e122d98b16d21f71ce69f379a9dc74bf85354f52.json delete mode 100644 .sqlx/query-fb111827f5db51855e8d67b2c72dc7078d390a041bb999e757b1cb7e78dc9de8.json create mode 100644 .sqlx/query-fb74bff60db4204bb7b88d4825254d76551d25a16eebec367d92f5ee987d2ade.json create mode 100644 .sqlx/query-fbeb58fe9142da315b1101249e6d0c83f4e7f42e239c67fb2780a2791beb849d.json delete mode 100644 .sqlx/query-fd8754bc010176d57db6441658aa832f1fb464ae29bac5f0623b4f1766b719c2.json delete mode 100644 .sqlx/query-fdcb5e63baa1eead3f4d91138dcb3062128639546a2e1bac4d8f6fccf6e16bc1.json create mode 100644 src/cli/codegen_cmd.rs create mode 100644 src/dto/page.rs create mode 100644 src/dto/reusable_block.rs create mode 100644 src/dto/tenant.rs create mode 100644 templates/codegen/model.rs.tera diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..bf4da423 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +SWAGGER_UI_DOWNLOAD_URL = "file:///Users/chriszhong/work/www/Rust/rust-blog/.local-cache/swagger-ui-v5.17.14.zip" diff --git a/.gitignore b/.gitignore index 97c1c9c4..681cbeb4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.db data/ backups/ +.local-cache/ node_modules *.pid diff --git a/.sqlx/query-002f8f9b95de626bf0382c30ae57bdc1127ad17048bce48ea53c3fb13311d202.json b/.sqlx/query-002f8f9b95de626bf0382c30ae57bdc1127ad17048bce48ea53c3fb13311d202.json new file mode 100644 index 00000000..bfcbad7d --- /dev/null +++ b/.sqlx/query-002f8f9b95de626bf0382c30ae57bdc1127ad17048bce48ea53c3fb13311d202.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM webhook_subscriptions WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "002f8f9b95de626bf0382c30ae57bdc1127ad17048bce48ea53c3fb13311d202" +} diff --git a/.sqlx/query-00f6bad9968219ec438c35106c208e4c4d2c1c45f2bbffa52e54ac4a34f96519.json b/.sqlx/query-00f6bad9968219ec438c35106c208e4c4d2c1c45f2bbffa52e54ac4a34f96519.json new file mode 100644 index 00000000..bc19ae95 --- /dev/null +++ b/.sqlx/query-00f6bad9968219ec438c35106c208e4c4d2c1c45f2bbffa52e54ac4a34f96519.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO reusable_blocks (id, name, block_type, content, description, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "00f6bad9968219ec438c35106c208e4c4d2c1c45f2bbffa52e54ac4a34f96519" +} diff --git a/.sqlx/query-0218fef502ae157fa71b54e879f03778459a13f196e241481b5ff2289c9861a1.json b/.sqlx/query-0218fef502ae157fa71b54e879f03778459a13f196e241481b5ff2289c9861a1.json new file mode 100644 index 00000000..f4810c62 --- /dev/null +++ b/.sqlx/query-0218fef502ae157fa71b54e879f03778459a13f196e241481b5ff2289c9861a1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO wallet_outbox (id, user_id, currency, amount, entry_type, tx_type, transaction_no, reference_type, reference_id, metadata, status, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "0218fef502ae157fa71b54e879f03778459a13f196e241481b5ff2289c9861a1" +} diff --git a/.sqlx/query-05732b48dbf7a440862fda2158c07b94ab60a33891316c437e88f8b2ac3aef5d.json b/.sqlx/query-05732b48dbf7a440862fda2158c07b94ab60a33891316c437e88f8b2ac3aef5d.json new file mode 100644 index 00000000..89bf1db4 --- /dev/null +++ b/.sqlx/query-05732b48dbf7a440862fda2158c07b94ab60a33891316c437e88f8b2ac3aef5d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO pages (id, title, slug, content, blocks, meta_title, meta_description, og_image, template, parent_id, sort_order, status, created_by, updated_by, cover_image, published_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", + "describe": { + "columns": [], + "parameters": { + "Right": 18 + }, + "nullable": [] + }, + "hash": "05732b48dbf7a440862fda2158c07b94ab60a33891316c437e88f8b2ac3aef5d" +} diff --git a/.sqlx/query-05f14ae8bf8557a31721d064cb39cc8961ba7f5f7ab3ca6bf011446d48cc9c11.json b/.sqlx/query-05f14ae8bf8557a31721d064cb39cc8961ba7f5f7ab3ca6bf011446d48cc9c11.json deleted file mode 100644 index 588155c8..00000000 --- a/.sqlx/query-05f14ae8bf8557a31721d064cb39cc8961ba7f5f7ab3ca6bf011446d48cc9c11.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO tags (document_id, name, slug, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "05f14ae8bf8557a31721d064cb39cc8961ba7f5f7ab3ca6bf011446d48cc9c11" -} diff --git a/.sqlx/query-06cc780f1876253554647fef3b852efe7faed0cbd129af7dd34d3f6679a0498d.json b/.sqlx/query-06cc780f1876253554647fef3b852efe7faed0cbd129af7dd34d3f6679a0498d.json new file mode 100644 index 00000000..a641ec13 --- /dev/null +++ b/.sqlx/query-06cc780f1876253554647fef3b852efe7faed0cbd129af7dd34d3f6679a0498d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM users WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "06cc780f1876253554647fef3b852efe7faed0cbd129af7dd34d3f6679a0498d" +} diff --git a/.sqlx/query-0745f93cf3ba9ee9a8763e8f609d59d31c4feade2e729ab5934ad2df2709fb34.json b/.sqlx/query-0745f93cf3ba9ee9a8763e8f609d59d31c4feade2e729ab5934ad2df2709fb34.json new file mode 100644 index 00000000..3964d722 --- /dev/null +++ b/.sqlx/query-0745f93cf3ba9ee9a8763e8f609d59d31c4feade2e729ab5934ad2df2709fb34.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users (id, username, created_at, updated_at, role, status, registered_via) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "0745f93cf3ba9ee9a8763e8f609d59d31c4feade2e729ab5934ad2df2709fb34" +} diff --git a/.sqlx/query-090094cc844e65a54240b25e553bb3045c8ef74f270ff32d1d7010550233b71f.json b/.sqlx/query-090094cc844e65a54240b25e553bb3045c8ef74f270ff32d1d7010550233b71f.json new file mode 100644 index 00000000..b97849e4 --- /dev/null +++ b/.sqlx/query-090094cc844e65a54240b25e553bb3045c8ef74f270ff32d1d7010550233b71f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO posts (id, title, slug, content, excerpt, cover_image, status, created_by, updated_by, category_id, published_at, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "090094cc844e65a54240b25e553bb3045c8ef74f270ff32d1d7010550233b71f" +} diff --git a/.sqlx/query-09a8ac8bc87bb8f221593dcbb39b4996d3782dd360c6f5cd5a24b5775335c44a.json b/.sqlx/query-09a8ac8bc87bb8f221593dcbb39b4996d3782dd360c6f5cd5a24b5775335c44a.json new file mode 100644 index 00000000..eec5c88b --- /dev/null +++ b/.sqlx/query-09a8ac8bc87bb8f221593dcbb39b4996d3782dd360c6f5cd5a24b5775335c44a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO product_variants (id, product_id, sku, title, price, original_price, stock, attributes, sort_order, is_active, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "09a8ac8bc87bb8f221593dcbb39b4996d3782dd360c6f5cd5a24b5775335c44a" +} diff --git a/.sqlx/query-0d9cf697203f12beace57613de3119777c9bd76a94b971019c5f09a1b639b211.json b/.sqlx/query-0d9cf697203f12beace57613de3119777c9bd76a94b971019c5f09a1b639b211.json new file mode 100644 index 00000000..6cc9ecf9 --- /dev/null +++ b/.sqlx/query-0d9cf697203f12beace57613de3119777c9bd76a94b971019c5f09a1b639b211.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_refunds (id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, payment_tx_id, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "0d9cf697203f12beace57613de3119777c9bd76a94b971019c5f09a1b639b211" +} diff --git a/.sqlx/query-0f7df02f4e4e2d810a2fec17a14bf23024dde72e97b064426ad3072f00ba0133.json b/.sqlx/query-0f7df02f4e4e2d810a2fec17a14bf23024dde72e97b064426ad3072f00ba0133.json new file mode 100644 index 00000000..a11d1190 --- /dev/null +++ b/.sqlx/query-0f7df02f4e4e2d810a2fec17a14bf23024dde72e97b064426ad3072f00ba0133.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "0f7df02f4e4e2d810a2fec17a14bf23024dde72e97b064426ad3072f00ba0133" +} diff --git a/.sqlx/query-0ffb5489ffefc25cfc4e75e33b1385e000e53257c840a1a0f8ed245e296c0895.json b/.sqlx/query-0ffb5489ffefc25cfc4e75e33b1385e000e53257c840a1a0f8ed245e296c0895.json deleted file mode 100644 index d2b818b1..00000000 --- a/.sqlx/query-0ffb5489ffefc25cfc4e75e33b1385e000e53257c840a1a0f8ed245e296c0895.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO posts (document_id, title, slug, content, excerpt, cover_image, status, created_by, updated_by, category_id, published_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "0ffb5489ffefc25cfc4e75e33b1385e000e53257c840a1a0f8ed245e296c0895" -} diff --git a/.sqlx/query-10ec8354fb857f2524cf7f8f56deade3f7c99a9c6094248ef22a3ae204bb789b.json b/.sqlx/query-10ec8354fb857f2524cf7f8f56deade3f7c99a9c6094248ef22a3ae204bb789b.json deleted file mode 100644 index 39733773..00000000 --- a/.sqlx/query-10ec8354fb857f2524cf7f8f56deade3f7c99a9c6094248ef22a3ae204bb789b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO users (document_id, username, created_at, updated_at, role, status, registered_via) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "10ec8354fb857f2524cf7f8f56deade3f7c99a9c6094248ef22a3ae204bb789b" -} diff --git a/.sqlx/query-118ed2067882c9119430926e6c1e9e03e36c853824a2ca7697f890d6a6a37b2b.json b/.sqlx/query-118ed2067882c9119430926e6c1e9e03e36c853824a2ca7697f890d6a6a37b2b.json deleted file mode 100644 index 8efdc387..00000000 --- a/.sqlx/query-118ed2067882c9119430926e6c1e9e03e36c853824a2ca7697f890d6a6a37b2b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO orders (document_id, user_id, order_no, subtotal, discount_amount, shipping_amount, total_amount, currency, buyer_name, buyer_phone, buyer_email, shipping_address, remark, tax_amount, coupon_id, shipping_address_id, billing_address_id, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", - "describe": { - "columns": [], - "parameters": { - "Right": 20 - }, - "nullable": [] - }, - "hash": "118ed2067882c9119430926e6c1e9e03e36c853824a2ca7697f890d6a6a37b2b" -} diff --git a/.sqlx/query-14bfe946a89dfafed01d3a351e0c76ab58184487ffc5f4145c76210a0828e09a.json b/.sqlx/query-14bfe946a89dfafed01d3a351e0c76ab58184487ffc5f4145c76210a0828e09a.json deleted file mode 100644 index 79981eb8..00000000 --- a/.sqlx/query-14bfe946a89dfafed01d3a351e0c76ab58184487ffc5f4145c76210a0828e09a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM webhook_subscriptions WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "14bfe946a89dfafed01d3a351e0c76ab58184487ffc5f4145c76210a0828e09a" -} diff --git a/.sqlx/query-1891058a8e02bf3890a7fb79e156df8ba82f145544e6c3f34a236782ff0f36f8.json b/.sqlx/query-1891058a8e02bf3890a7fb79e156df8ba82f145544e6c3f34a236782ff0f36f8.json deleted file mode 100644 index 99dd3670..00000000 --- a/.sqlx/query-1891058a8e02bf3890a7fb79e156df8ba82f145544e6c3f34a236782ff0f36f8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO refresh_tokens (document_id, user_id, token, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "1891058a8e02bf3890a7fb79e156df8ba82f145544e6c3f34a236782ff0f36f8" -} diff --git a/.sqlx/query-1a6350d43916712692fd0b7abc6ba41a6df71a318dcfdd3258827eb1d1b04f6e.json b/.sqlx/query-1a6350d43916712692fd0b7abc6ba41a6df71a318dcfdd3258827eb1d1b04f6e.json new file mode 100644 index 00000000..e014ece4 --- /dev/null +++ b/.sqlx/query-1a6350d43916712692fd0b7abc6ba41a6df71a318dcfdd3258827eb1d1b04f6e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO api_tokens (id, user_id, name, token_hash, token_prefix, scopes, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "1a6350d43916712692fd0b7abc6ba41a6df71a318dcfdd3258827eb1d1b04f6e" +} diff --git a/.sqlx/query-1a66e50cdcdce85103ef5a84e93d8035746e68df2bfabc61d71382f92785f7cb.json b/.sqlx/query-1a66e50cdcdce85103ef5a84e93d8035746e68df2bfabc61d71382f92785f7cb.json deleted file mode 100644 index 3f70ea56..00000000 --- a/.sqlx/query-1a66e50cdcdce85103ef5a84e93d8035746e68df2bfabc61d71382f92785f7cb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO api_tokens (document_id, user_id, name, token_hash, token_prefix, scopes, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "1a66e50cdcdce85103ef5a84e93d8035746e68df2bfabc61d71382f92785f7cb" -} diff --git a/.sqlx/query-1a804863d5148834f4290bb2de855d13787e0d15c492dffe8b9e3449217e4290.json b/.sqlx/query-1a804863d5148834f4290bb2de855d13787e0d15c492dffe8b9e3449217e4290.json new file mode 100644 index 00000000..487b1839 --- /dev/null +++ b/.sqlx/query-1a804863d5148834f4290bb2de855d13787e0d15c492dffe8b9e3449217e4290.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO order_items (id, order_id, product_id, variant_id, title, description, sku, unit_price, quantity, subtotal, tax_amount, cover_url, attributes, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "1a804863d5148834f4290bb2de855d13787e0d15c492dffe8b9e3449217e4290" +} diff --git a/.sqlx/query-1f0c8777464de4aa46699f24fd19643f2e4b71650f2b1291ac7f106448e9ffa5.json b/.sqlx/query-1f0c8777464de4aa46699f24fd19643f2e4b71650f2b1291ac7f106448e9ffa5.json deleted file mode 100644 index cd24f17e..00000000 --- a/.sqlx/query-1f0c8777464de4aa46699f24fd19643f2e4b71650f2b1291ac7f106448e9ffa5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO wallets (document_id, user_id, currency, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "1f0c8777464de4aa46699f24fd19643f2e4b71650f2b1291ac7f106448e9ffa5" -} diff --git a/.sqlx/query-2484b7be5ea6d715276744baeda8a30d832371735dc44f776bbffc9af6c27034.json b/.sqlx/query-2484b7be5ea6d715276744baeda8a30d832371735dc44f776bbffc9af6c27034.json deleted file mode 100644 index 8da239bf..00000000 --- a/.sqlx/query-2484b7be5ea6d715276744baeda8a30d832371735dc44f776bbffc9af6c27034.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO sms_codes (document_id, phone, code, purpose, expires_at, ip_address, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "2484b7be5ea6d715276744baeda8a30d832371735dc44f776bbffc9af6c27034" -} diff --git a/.sqlx/query-2550524dc2d2b8188b8845dc2af35551d5e7ff56e29cfb0bd7122f4909bf7020.json b/.sqlx/query-2550524dc2d2b8188b8845dc2af35551d5e7ff56e29cfb0bd7122f4909bf7020.json deleted file mode 100644 index d4ea6d53..00000000 --- a/.sqlx/query-2550524dc2d2b8188b8845dc2af35551d5e7ff56e29cfb0bd7122f4909bf7020.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO reusable_blocks (document_id, name, block_type, content, description, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "2550524dc2d2b8188b8845dc2af35551d5e7ff56e29cfb0bd7122f4909bf7020" -} diff --git a/.sqlx/query-269bfe69eadc1f77125ede936102a43e65fc8e54ac0e31dc25876ed11e6a75c6.json b/.sqlx/query-269bfe69eadc1f77125ede936102a43e65fc8e54ac0e31dc25876ed11e6a75c6.json new file mode 100644 index 00000000..8583d296 --- /dev/null +++ b/.sqlx/query-269bfe69eadc1f77125ede936102a43e65fc8e54ac0e31dc25876ed11e6a75c6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO currencies (id, code, name, decimals, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "269bfe69eadc1f77125ede936102a43e65fc8e54ac0e31dc25876ed11e6a75c6" +} diff --git a/.sqlx/query-276f1d700d4f7f1964fd3750cb4b0a0d8a13ba6df8a3538f2351143866e46afd.json b/.sqlx/query-276f1d700d4f7f1964fd3750cb4b0a0d8a13ba6df8a3538f2351143866e46afd.json deleted file mode 100644 index bdcbaf04..00000000 --- a/.sqlx/query-276f1d700d4f7f1964fd3750cb4b0a0d8a13ba6df8a3538f2351143866e46afd.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM cart_items WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "276f1d700d4f7f1964fd3750cb4b0a0d8a13ba6df8a3538f2351143866e46afd" -} diff --git a/.sqlx/query-28a6772da782aeab44de5b1ab38dcfdaf596ff1b172b29d02008f38ec680befc.json b/.sqlx/query-28a6772da782aeab44de5b1ab38dcfdaf596ff1b172b29d02008f38ec680befc.json new file mode 100644 index 00000000..9862ef30 --- /dev/null +++ b/.sqlx/query-28a6772da782aeab44de5b1ab38dcfdaf596ff1b172b29d02008f38ec680befc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO audit_log (id, actor_id, actor_role, action, subject, subject_id, detail, ip_address, user_agent, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "28a6772da782aeab44de5b1ab38dcfdaf596ff1b172b29d02008f38ec680befc" +} diff --git a/.sqlx/query-2919b493c38a046808ace3dfc0bb9f3b75ed64146de924cd5410374b6e560ffb.json b/.sqlx/query-2919b493c38a046808ace3dfc0bb9f3b75ed64146de924cd5410374b6e560ffb.json new file mode 100644 index 00000000..29e0ea7e --- /dev/null +++ b/.sqlx/query-2919b493c38a046808ace3dfc0bb9f3b75ed64146de924cd5410374b6e560ffb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO pages (id, title, slug, content, blocks, meta_title, meta_description, og_image, template, parent_id, sort_order, status, created_by, updated_by, cover_image, published_at, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", + "describe": { + "columns": [], + "parameters": { + "Right": 19 + }, + "nullable": [] + }, + "hash": "2919b493c38a046808ace3dfc0bb9f3b75ed64146de924cd5410374b6e560ffb" +} diff --git a/.sqlx/query-295429ee6c6e6a6a53e2942286825522d8a6d4ae3cd90d12955d9083b4532ca9.json b/.sqlx/query-295429ee6c6e6a6a53e2942286825522d8a6d4ae3cd90d12955d9083b4532ca9.json new file mode 100644 index 00000000..51c54b1f --- /dev/null +++ b/.sqlx/query-295429ee6c6e6a6a53e2942286825522d8a6d4ae3cd90d12955d9083b4532ca9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO oauth_states (id, provider, code_verifier, user_id, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "295429ee6c6e6a6a53e2942286825522d8a6d4ae3cd90d12955d9083b4532ca9" +} diff --git a/.sqlx/query-29ec564f478f99e306a62409f06d01d638cce4e957266188c7828dc83b9c1c95.json b/.sqlx/query-29ec564f478f99e306a62409f06d01d638cce4e957266188c7828dc83b9c1c95.json new file mode 100644 index 00000000..82e9fb51 --- /dev/null +++ b/.sqlx/query-29ec564f478f99e306a62409f06d01d638cce4e957266188c7828dc83b9c1c95.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO cart_items (id, user_id, product_id, variant_id, quantity, attributes, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "29ec564f478f99e306a62409f06d01d638cce4e957266188c7828dc83b9c1c95" +} diff --git a/.sqlx/query-2a99974dd884c439c750b857f54631d370f2c20ca7e76b43f226a4dd09c130f0.json b/.sqlx/query-2a99974dd884c439c750b857f54631d370f2c20ca7e76b43f226a4dd09c130f0.json new file mode 100644 index 00000000..593724d3 --- /dev/null +++ b/.sqlx/query-2a99974dd884c439c750b857f54631d370f2c20ca7e76b43f226a4dd09c130f0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO order_items (id, order_id, product_id, variant_id, title, description, sku, unit_price, quantity, subtotal, tax_amount, cover_url, attributes, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", + "describe": { + "columns": [], + "parameters": { + "Right": 15 + }, + "nullable": [] + }, + "hash": "2a99974dd884c439c750b857f54631d370f2c20ca7e76b43f226a4dd09c130f0" +} diff --git a/.sqlx/query-347ea867704d076b7cc725da4bf2122a3df4e65b776327b6d8ae61105f680dab.json b/.sqlx/query-347ea867704d076b7cc725da4bf2122a3df4e65b776327b6d8ae61105f680dab.json new file mode 100644 index 00000000..8571c135 --- /dev/null +++ b/.sqlx/query-347ea867704d076b7cc725da4bf2122a3df4e65b776327b6d8ae61105f680dab.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM users WHERE id = ?1 AND tenant_id = ?2", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "347ea867704d076b7cc725da4bf2122a3df4e65b776327b6d8ae61105f680dab" +} diff --git a/.sqlx/query-3a7ddd71e735af46194a8d65fe4c821eaa411367d12e490abea5f787d927d765.json b/.sqlx/query-3a7ddd71e735af46194a8d65fe4c821eaa411367d12e490abea5f787d927d765.json deleted file mode 100644 index ff748bc1..00000000 --- a/.sqlx/query-3a7ddd71e735af46194a8d65fe4c821eaa411367d12e490abea5f787d927d765.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO media (document_id, user_id, filename, filepath, mimetype, size, width, height, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "3a7ddd71e735af46194a8d65fe4c821eaa411367d12e490abea5f787d927d765" -} diff --git a/.sqlx/query-3b5a2cbe347a7580d9a86a6397700a362791b56b0022249e60293f8f8f766b6e.json b/.sqlx/query-3b5a2cbe347a7580d9a86a6397700a362791b56b0022249e60293f8f8f766b6e.json deleted file mode 100644 index d565f916..00000000 --- a/.sqlx/query-3b5a2cbe347a7580d9a86a6397700a362791b56b0022249e60293f8f8f766b6e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO comments (document_id, post_id, created_by, updated_by, nickname, email, content, parent_id, created_at, updated_at, status, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "3b5a2cbe347a7580d9a86a6397700a362791b56b0022249e60293f8f8f766b6e" -} diff --git a/.sqlx/query-3ec645d617cb339c01ed71f621ebd58962be98f0436de25401971c037f49f497.json b/.sqlx/query-3ec645d617cb339c01ed71f621ebd58962be98f0436de25401971c037f49f497.json deleted file mode 100644 index a6d461bd..00000000 --- a/.sqlx/query-3ec645d617cb339c01ed71f621ebd58962be98f0436de25401971c037f49f497.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_channels (document_id, provider, name, is_live, credentials, webhook_secret, settings, is_active, sort_order, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "3ec645d617cb339c01ed71f621ebd58962be98f0436de25401971c037f49f497" -} diff --git a/.sqlx/query-3eca34da3315899e2c42270e042069931238caa8aa1a77275217a01142061324.json b/.sqlx/query-3eca34da3315899e2c42270e042069931238caa8aa1a77275217a01142061324.json deleted file mode 100644 index 7efba1c4..00000000 --- a/.sqlx/query-3eca34da3315899e2c42270e042069931238caa8aa1a77275217a01142061324.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO tags (document_id, name, slug, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "3eca34da3315899e2c42270e042069931238caa8aa1a77275217a01142061324" -} diff --git a/.sqlx/query-3f5e106e8664eac53a52b59f3724cd4a4b6d9846effe43beec8f7d075fac6976.json b/.sqlx/query-3f5e106e8664eac53a52b59f3724cd4a4b6d9846effe43beec8f7d075fac6976.json deleted file mode 100644 index 595471cc..00000000 --- a/.sqlx/query-3f5e106e8664eac53a52b59f3724cd4a4b6d9846effe43beec8f7d075fac6976.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO wallet_outbox (document_id, user_id, currency, amount, entry_type, tx_type, transaction_no, reference_type, reference_id, metadata, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "3f5e106e8664eac53a52b59f3724cd4a4b6d9846effe43beec8f7d075fac6976" -} diff --git a/.sqlx/query-410b34079c9d2e443b87e0b5b0c7f15513e3372366172ef010cf08e498994c8b.json b/.sqlx/query-410b34079c9d2e443b87e0b5b0c7f15513e3372366172ef010cf08e498994c8b.json new file mode 100644 index 00000000..d549b910 --- /dev/null +++ b/.sqlx/query-410b34079c9d2e443b87e0b5b0c7f15513e3372366172ef010cf08e498994c8b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO permissions (id, role_id, action, subject, fields, conditions, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "410b34079c9d2e443b87e0b5b0c7f15513e3372366172ef010cf08e498994c8b" +} diff --git a/.sqlx/query-42102721fa299de81560fb549e283e01d7b28e92c4b26f1aa45af22fba412837.json b/.sqlx/query-42102721fa299de81560fb549e283e01d7b28e92c4b26f1aa45af22fba412837.json deleted file mode 100644 index 48c0c84e..00000000 --- a/.sqlx/query-42102721fa299de81560fb549e283e01d7b28e92c4b26f1aa45af22fba412837.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO comments (document_id, post_id, created_by, updated_by, nickname, email, content, parent_id, created_at, updated_at, status) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "42102721fa299de81560fb549e283e01d7b28e92c4b26f1aa45af22fba412837" -} diff --git a/.sqlx/query-48321ccbd123a9ffccc9ebbda3c7e71b001c9600d343da1e3fb005aa6849e55a.json b/.sqlx/query-48321ccbd123a9ffccc9ebbda3c7e71b001c9600d343da1e3fb005aa6849e55a.json deleted file mode 100644 index fd5d0b1a..00000000 --- a/.sqlx/query-48321ccbd123a9ffccc9ebbda3c7e71b001c9600d343da1e3fb005aa6849e55a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "48321ccbd123a9ffccc9ebbda3c7e71b001c9600d343da1e3fb005aa6849e55a" -} diff --git a/.sqlx/query-4b747395c48682c15da802e576e637dfa6d697206896e963abd2281ba910619a.json b/.sqlx/query-4b747395c48682c15da802e576e637dfa6d697206896e963abd2281ba910619a.json new file mode 100644 index 00000000..ba75dfd5 --- /dev/null +++ b/.sqlx/query-4b747395c48682c15da802e576e637dfa6d697206896e963abd2281ba910619a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_transactions (id, payment_order_id, order_id, user_id, tx_type, amount, currency, provider_tx_id, status, raw_payload, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "4b747395c48682c15da802e576e637dfa6d697206896e963abd2281ba910619a" +} diff --git a/.sqlx/query-4d173dc00623dbb15d982fa725d1082a3459e408b4d3e9d7c1f4dd6408897010.json b/.sqlx/query-4d173dc00623dbb15d982fa725d1082a3459e408b4d3e9d7c1f4dd6408897010.json new file mode 100644 index 00000000..6a69a470 --- /dev/null +++ b/.sqlx/query-4d173dc00623dbb15d982fa725d1082a3459e408b4d3e9d7c1f4dd6408897010.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO media (id, user_id, filename, filepath, mimetype, size, width, height, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "4d173dc00623dbb15d982fa725d1082a3459e408b4d3e9d7c1f4dd6408897010" +} diff --git a/.sqlx/query-4e49d760f473b689691830c73f52e4449df61bb999e90876e89efe1c0d78a4f0.json b/.sqlx/query-4e49d760f473b689691830c73f52e4449df61bb999e90876e89efe1c0d78a4f0.json deleted file mode 100644 index 33f4227a..00000000 --- a/.sqlx/query-4e49d760f473b689691830c73f52e4449df61bb999e90876e89efe1c0d78a4f0.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_orders (document_id, user_id, order_id, title, amount, currency, channel_id, provider, reference_type, reference_id, return_url, idempotency_key, client_ip, client_language, client_country, client_user_agent, channel_selected_by, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", - "describe": { - "columns": [], - "parameters": { - "Right": 20 - }, - "nullable": [] - }, - "hash": "4e49d760f473b689691830c73f52e4449df61bb999e90876e89efe1c0d78a4f0" -} diff --git a/.sqlx/query-5295a282c7156cc458c9c8261caf70bf9638cc9dd2543eef84f3c81b74f80542.json b/.sqlx/query-5295a282c7156cc458c9c8261caf70bf9638cc9dd2543eef84f3c81b74f80542.json deleted file mode 100644 index c9b248a4..00000000 --- a/.sqlx/query-5295a282c7156cc458c9c8261caf70bf9638cc9dd2543eef84f3c81b74f80542.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO currencies (document_id, code, name, decimals, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "5295a282c7156cc458c9c8261caf70bf9638cc9dd2543eef84f3c81b74f80542" -} diff --git a/.sqlx/query-5758dc7c2634b3a00cf929c79df1a24e6834a0da52952a5b2f21221681886d7d.json b/.sqlx/query-5758dc7c2634b3a00cf929c79df1a24e6834a0da52952a5b2f21221681886d7d.json new file mode 100644 index 00000000..c7d5a636 --- /dev/null +++ b/.sqlx/query-5758dc7c2634b3a00cf929c79df1a24e6834a0da52952a5b2f21221681886d7d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO webhook_subscriptions (id, url, secret, events, enabled, description, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "5758dc7c2634b3a00cf929c79df1a24e6834a0da52952a5b2f21221681886d7d" +} diff --git a/.sqlx/query-57c76d4195d5a4778ffdd1374bd9160013287aa0cbbab8c5204b5dbe3e54a3c3.json b/.sqlx/query-57c76d4195d5a4778ffdd1374bd9160013287aa0cbbab8c5204b5dbe3e54a3c3.json new file mode 100644 index 00000000..4260b4c5 --- /dev/null +++ b/.sqlx/query-57c76d4195d5a4778ffdd1374bd9160013287aa0cbbab8c5204b5dbe3e54a3c3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO oauth_accounts (id, user_id, provider, provider_user_id, email, display_name, avatar_url, access_token, refresh_token, token_expires_at, profile, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "57c76d4195d5a4778ffdd1374bd9160013287aa0cbbab8c5204b5dbe3e54a3c3" +} diff --git a/.sqlx/query-5a101242e9a58a8e8c028579d8fafc1d5ff18fd66510b25cabccbcc581c76e30.json b/.sqlx/query-5a101242e9a58a8e8c028579d8fafc1d5ff18fd66510b25cabccbcc581c76e30.json new file mode 100644 index 00000000..df7c8620 --- /dev/null +++ b/.sqlx/query-5a101242e9a58a8e8c028579d8fafc1d5ff18fd66510b25cabccbcc581c76e30.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM cart_items WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "5a101242e9a58a8e8c028579d8fafc1d5ff18fd66510b25cabccbcc581c76e30" +} diff --git a/.sqlx/query-5ce3f0fefb05bf54aeca419eda8c0d35586cd07a24fd6232df173e3ffc2b69f7.json b/.sqlx/query-5ce3f0fefb05bf54aeca419eda8c0d35586cd07a24fd6232df173e3ffc2b69f7.json new file mode 100644 index 00000000..fc255b1a --- /dev/null +++ b/.sqlx/query-5ce3f0fefb05bf54aeca419eda8c0d35586cd07a24fd6232df173e3ffc2b69f7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_transactions (id, payment_order_id, order_id, user_id, tx_type, amount, currency, provider_tx_id, status, raw_payload, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "5ce3f0fefb05bf54aeca419eda8c0d35586cd07a24fd6232df173e3ffc2b69f7" +} diff --git a/.sqlx/query-60dc717206e2519ccacbbb8c4e9f273da0d49943754ebf8e715988cea21fe3ae.json b/.sqlx/query-60dc717206e2519ccacbbb8c4e9f273da0d49943754ebf8e715988cea21fe3ae.json new file mode 100644 index 00000000..b6e1683d --- /dev/null +++ b/.sqlx/query-60dc717206e2519ccacbbb8c4e9f273da0d49943754ebf8e715988cea21fe3ae.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO user_addresses (id, user_id, label, recipient_name, phone, country, province, city, district, address_line1, address_line2, postal_code, is_default, address_type, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", + "describe": { + "columns": [], + "parameters": { + "Right": 16 + }, + "nullable": [] + }, + "hash": "60dc717206e2519ccacbbb8c4e9f273da0d49943754ebf8e715988cea21fe3ae" +} diff --git a/.sqlx/query-619bbdf9be92fbc7252df50e6900b32a67eedf844d63c3192a1134bcc84d780b.json b/.sqlx/query-619bbdf9be92fbc7252df50e6900b32a67eedf844d63c3192a1134bcc84d780b.json new file mode 100644 index 00000000..dd244c3e --- /dev/null +++ b/.sqlx/query-619bbdf9be92fbc7252df50e6900b32a67eedf844d63c3192a1134bcc84d780b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO comments (id, post_id, created_by, updated_by, nickname, email, content, parent_id, created_at, updated_at, status) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "619bbdf9be92fbc7252df50e6900b32a67eedf844d63c3192a1134bcc84d780b" +} diff --git a/.sqlx/query-651405c96c6d0ca6ab5e56fb4c8c79b6f86ab3f05585608732ff011c65a01a48.json b/.sqlx/query-651405c96c6d0ca6ab5e56fb4c8c79b6f86ab3f05585608732ff011c65a01a48.json deleted file mode 100644 index a8942bc5..00000000 --- a/.sqlx/query-651405c96c6d0ca6ab5e56fb4c8c79b6f86ab3f05585608732ff011c65a01a48.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO user_addresses (document_id, user_id, label, recipient_name, phone, country, province, city, district, address_line1, address_line2, postal_code, is_default, address_type, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", - "describe": { - "columns": [], - "parameters": { - "Right": 17 - }, - "nullable": [] - }, - "hash": "651405c96c6d0ca6ab5e56fb4c8c79b6f86ab3f05585608732ff011c65a01a48" -} diff --git a/.sqlx/query-69ac6a75bbfdd641366092f22e55fad8e863f216e92257722c2a271ba1d8dcf3.json b/.sqlx/query-69ac6a75bbfdd641366092f22e55fad8e863f216e92257722c2a271ba1d8dcf3.json deleted file mode 100644 index fa936578..00000000 --- a/.sqlx/query-69ac6a75bbfdd641366092f22e55fad8e863f216e92257722c2a271ba1d8dcf3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO user_addresses (document_id, user_id, label, recipient_name, phone, country, province, city, district, address_line1, address_line2, postal_code, is_default, address_type, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", - "describe": { - "columns": [], - "parameters": { - "Right": 16 - }, - "nullable": [] - }, - "hash": "69ac6a75bbfdd641366092f22e55fad8e863f216e92257722c2a271ba1d8dcf3" -} diff --git a/.sqlx/query-6b15cd2e3955588956ba22aa093cb605e246efe53cc29338388d8728fa432e57.json b/.sqlx/query-6b15cd2e3955588956ba22aa093cb605e246efe53cc29338388d8728fa432e57.json new file mode 100644 index 00000000..13697db8 --- /dev/null +++ b/.sqlx/query-6b15cd2e3955588956ba22aa093cb605e246efe53cc29338388d8728fa432e57.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_refunds (id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "6b15cd2e3955588956ba22aa093cb605e246efe53cc29338388d8728fa432e57" +} diff --git a/.sqlx/query-6e0ec4602996acf7060db60846f1b3c9a7d123b5172b5bb416f254dc6baa4417.json b/.sqlx/query-6e0ec4602996acf7060db60846f1b3c9a7d123b5172b5bb416f254dc6baa4417.json deleted file mode 100644 index e1ade27c..00000000 --- a/.sqlx/query-6e0ec4602996acf7060db60846f1b3c9a7d123b5172b5bb416f254dc6baa4417.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO audit_log (document_id, actor_id, actor_role, action, subject, subject_id, detail, ip_address, user_agent, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "6e0ec4602996acf7060db60846f1b3c9a7d123b5172b5bb416f254dc6baa4417" -} diff --git a/.sqlx/query-6e1f6b059bf443b532ec684286a965de18bbfe6b0b3e4b355da425111ee861b5.json b/.sqlx/query-6e1f6b059bf443b532ec684286a965de18bbfe6b0b3e4b355da425111ee861b5.json deleted file mode 100644 index 9dcadc5f..00000000 --- a/.sqlx/query-6e1f6b059bf443b532ec684286a965de18bbfe6b0b3e4b355da425111ee861b5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_refunds (document_id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "6e1f6b059bf443b532ec684286a965de18bbfe6b0b3e4b355da425111ee861b5" -} diff --git a/.sqlx/query-7015463e67a587c677bf5f192ba2aa2b75302feb99d520fdef4bb3a709d28379.json b/.sqlx/query-7015463e67a587c677bf5f192ba2aa2b75302feb99d520fdef4bb3a709d28379.json new file mode 100644 index 00000000..90b380d7 --- /dev/null +++ b/.sqlx/query-7015463e67a587c677bf5f192ba2aa2b75302feb99d520fdef4bb3a709d28379.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tags (id, name, slug, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "7015463e67a587c677bf5f192ba2aa2b75302feb99d520fdef4bb3a709d28379" +} diff --git a/.sqlx/query-701ef74f1650e40f6c128b1a8978c299a7dd62527ae871f137511d88d7619618.json b/.sqlx/query-701ef74f1650e40f6c128b1a8978c299a7dd62527ae871f137511d88d7619618.json deleted file mode 100644 index c63ce916..00000000 --- a/.sqlx/query-701ef74f1650e40f6c128b1a8978c299a7dd62527ae871f137511d88d7619618.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM api_tokens WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "701ef74f1650e40f6c128b1a8978c299a7dd62527ae871f137511d88d7619618" -} diff --git a/.sqlx/query-7051eadf2365278e99621914872204ec02d5abccf54481ef54e68eccf7ae11f9.json b/.sqlx/query-7051eadf2365278e99621914872204ec02d5abccf54481ef54e68eccf7ae11f9.json new file mode 100644 index 00000000..1a5cf358 --- /dev/null +++ b/.sqlx/query-7051eadf2365278e99621914872204ec02d5abccf54481ef54e68eccf7ae11f9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO reusable_blocks (id, name, block_type, content, description, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "7051eadf2365278e99621914872204ec02d5abccf54481ef54e68eccf7ae11f9" +} diff --git a/.sqlx/query-75a5da6ef797a9c43bb377cd90c1110954448b5ec490745547285373928ecc2f.json b/.sqlx/query-75a5da6ef797a9c43bb377cd90c1110954448b5ec490745547285373928ecc2f.json deleted file mode 100644 index c12466a3..00000000 --- a/.sqlx/query-75a5da6ef797a9c43bb377cd90c1110954448b5ec490745547285373928ecc2f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_refunds (document_id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "75a5da6ef797a9c43bb377cd90c1110954448b5ec490745547285373928ecc2f" -} diff --git a/.sqlx/query-779500a155e4d67dd7cf63a53e7dfbedbb382e5cebe261fd3267980d21c4d5b4.json b/.sqlx/query-779500a155e4d67dd7cf63a53e7dfbedbb382e5cebe261fd3267980d21c4d5b4.json new file mode 100644 index 00000000..9d000ae3 --- /dev/null +++ b/.sqlx/query-779500a155e4d67dd7cf63a53e7dfbedbb382e5cebe261fd3267980d21c4d5b4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO posts (id, title, slug, content, excerpt, cover_image, status, created_by, updated_by, category_id, published_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "779500a155e4d67dd7cf63a53e7dfbedbb382e5cebe261fd3267980d21c4d5b4" +} diff --git a/.sqlx/query-7943383ac8989afd5bc1b279fb2073700bd7ac970f3280f861401fcc272f8dbf.json b/.sqlx/query-7943383ac8989afd5bc1b279fb2073700bd7ac970f3280f861401fcc272f8dbf.json deleted file mode 100644 index 81adaf10..00000000 --- a/.sqlx/query-7943383ac8989afd5bc1b279fb2073700bd7ac970f3280f861401fcc272f8dbf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_orders (document_id, user_id, order_id, title, amount, currency, channel_id, provider, reference_type, reference_id, return_url, idempotency_key, client_ip, client_language, client_country, client_user_agent, channel_selected_by, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21)", - "describe": { - "columns": [], - "parameters": { - "Right": 21 - }, - "nullable": [] - }, - "hash": "7943383ac8989afd5bc1b279fb2073700bd7ac970f3280f861401fcc272f8dbf" -} diff --git a/.sqlx/query-79ad88673cd2323868f8f5d920d1535580c664613b491756da16620aef9779de.json b/.sqlx/query-79ad88673cd2323868f8f5d920d1535580c664613b491756da16620aef9779de.json deleted file mode 100644 index 724f1cff..00000000 --- a/.sqlx/query-79ad88673cd2323868f8f5d920d1535580c664613b491756da16620aef9779de.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "79ad88673cd2323868f8f5d920d1535580c664613b491756da16620aef9779de" -} diff --git a/.sqlx/query-7d4b62eba6f6bd5abf49894fb3feb87fd7ead53fd670cd0ba7bd94c51ba23ac9.json b/.sqlx/query-7d4b62eba6f6bd5abf49894fb3feb87fd7ead53fd670cd0ba7bd94c51ba23ac9.json deleted file mode 100644 index 212ad1a7..00000000 --- a/.sqlx/query-7d4b62eba6f6bd5abf49894fb3feb87fd7ead53fd670cd0ba7bd94c51ba23ac9.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO order_items (document_id, order_id, product_id, variant_id, title, description, sku, unit_price, quantity, subtotal, tax_amount, cover_url, attributes, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "7d4b62eba6f6bd5abf49894fb3feb87fd7ead53fd670cd0ba7bd94c51ba23ac9" -} diff --git a/.sqlx/query-7e106ee0128fc4597f89a594f3f72b504edf1ea8c4d20a60117b4c0a37e4ba33.json b/.sqlx/query-7e106ee0128fc4597f89a594f3f72b504edf1ea8c4d20a60117b4c0a37e4ba33.json new file mode 100644 index 00000000..796efd37 --- /dev/null +++ b/.sqlx/query-7e106ee0128fc4597f89a594f3f72b504edf1ea8c4d20a60117b4c0a37e4ba33.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO wallets (id, user_id, currency, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "7e106ee0128fc4597f89a594f3f72b504edf1ea8c4d20a60117b4c0a37e4ba33" +} diff --git a/.sqlx/query-802f6376046b4a50fc157f3cb132e9f57fb5fc74753c00b0f311a625e5fc2200.json b/.sqlx/query-802f6376046b4a50fc157f3cb132e9f57fb5fc74753c00b0f311a625e5fc2200.json deleted file mode 100644 index d819d06e..00000000 --- a/.sqlx/query-802f6376046b4a50fc157f3cb132e9f57fb5fc74753c00b0f311a625e5fc2200.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_transactions (document_id, payment_order_id, order_id, user_id, tx_type, amount, currency, provider_tx_id, status, raw_payload, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "802f6376046b4a50fc157f3cb132e9f57fb5fc74753c00b0f311a625e5fc2200" -} diff --git a/.sqlx/query-80d009ab24ccedf6aa87d6d5d166f20b2beb890624abac1f68cdb23755dd1373.json b/.sqlx/query-80d009ab24ccedf6aa87d6d5d166f20b2beb890624abac1f68cdb23755dd1373.json deleted file mode 100644 index 4d61a0a5..00000000 --- a/.sqlx/query-80d009ab24ccedf6aa87d6d5d166f20b2beb890624abac1f68cdb23755dd1373.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO cart_items (document_id, user_id, product_id, variant_id, quantity, attributes, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "80d009ab24ccedf6aa87d6d5d166f20b2beb890624abac1f68cdb23755dd1373" -} diff --git a/.sqlx/query-816d3e64cf350633d70c14241f20487a9210946793a8a41d6e2e7b8536c59559.json b/.sqlx/query-816d3e64cf350633d70c14241f20487a9210946793a8a41d6e2e7b8536c59559.json new file mode 100644 index 00000000..c3f925b7 --- /dev/null +++ b/.sqlx/query-816d3e64cf350633d70c14241f20487a9210946793a8a41d6e2e7b8536c59559.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_orders (id, user_id, order_id, title, amount, currency, channel_id, provider, reference_type, reference_id, return_url, idempotency_key, client_ip, client_language, client_country, client_user_agent, channel_selected_by, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", + "describe": { + "columns": [], + "parameters": { + "Right": 20 + }, + "nullable": [] + }, + "hash": "816d3e64cf350633d70c14241f20487a9210946793a8a41d6e2e7b8536c59559" +} diff --git a/.sqlx/query-85ddf6296df989403dd05ce8fce41dc77b26b68ca0ab101df4cf3a7ab21fe502.json b/.sqlx/query-85ddf6296df989403dd05ce8fce41dc77b26b68ca0ab101df4cf3a7ab21fe502.json new file mode 100644 index 00000000..579e5935 --- /dev/null +++ b/.sqlx/query-85ddf6296df989403dd05ce8fce41dc77b26b68ca0ab101df4cf3a7ab21fe502.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_channels (id, provider, name, is_live, credentials, webhook_secret, settings, is_active, sort_order, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "85ddf6296df989403dd05ce8fce41dc77b26b68ca0ab101df4cf3a7ab21fe502" +} diff --git a/.sqlx/query-8b4690ba53bdc8815dec7ce06c77505b8fd06507bdc16a31aaabdc21cbc33c5c.json b/.sqlx/query-8b4690ba53bdc8815dec7ce06c77505b8fd06507bdc16a31aaabdc21cbc33c5c.json new file mode 100644 index 00000000..c0997e88 --- /dev/null +++ b/.sqlx/query-8b4690ba53bdc8815dec7ce06c77505b8fd06507bdc16a31aaabdc21cbc33c5c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "8b4690ba53bdc8815dec7ce06c77505b8fd06507bdc16a31aaabdc21cbc33c5c" +} diff --git a/.sqlx/query-8d0d83f16e4ad0c7240ebf94971c40618bfa67fede2ebb5d5863319691147250.json b/.sqlx/query-8d0d83f16e4ad0c7240ebf94971c40618bfa67fede2ebb5d5863319691147250.json deleted file mode 100644 index 7fb15426..00000000 --- a/.sqlx/query-8d0d83f16e4ad0c7240ebf94971c40618bfa67fede2ebb5d5863319691147250.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO content_revisions (document_id, content_type, record_id, revision_number, snapshot, created_by, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "8d0d83f16e4ad0c7240ebf94971c40618bfa67fede2ebb5d5863319691147250" -} diff --git a/.sqlx/query-8f84d596eb12222a16f3137fb2f5e22d8585d2550bb5ea22761294cc57432d9f.json b/.sqlx/query-8f84d596eb12222a16f3137fb2f5e22d8585d2550bb5ea22761294cc57432d9f.json new file mode 100644 index 00000000..16c0b267 --- /dev/null +++ b/.sqlx/query-8f84d596eb12222a16f3137fb2f5e22d8585d2550bb5ea22761294cc57432d9f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO media (id, user_id, filename, filepath, mimetype, size, width, height, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "8f84d596eb12222a16f3137fb2f5e22d8585d2550bb5ea22761294cc57432d9f" +} diff --git a/.sqlx/query-8fa80a24d1be10f16c3ab45822768833de5aec68c78727c675486613e5671889.json b/.sqlx/query-8fa80a24d1be10f16c3ab45822768833de5aec68c78727c675486613e5671889.json deleted file mode 100644 index 8826e5bc..00000000 --- a/.sqlx/query-8fa80a24d1be10f16c3ab45822768833de5aec68c78727c675486613e5671889.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM roles WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "8fa80a24d1be10f16c3ab45822768833de5aec68c78727c675486613e5671889" -} diff --git a/.sqlx/query-927486c9db9989a3187eea43ca6c55c49d1b9fc4c7af6e2f8ca3b198a9341014.json b/.sqlx/query-927486c9db9989a3187eea43ca6c55c49d1b9fc4c7af6e2f8ca3b198a9341014.json deleted file mode 100644 index 1b6f4191..00000000 --- a/.sqlx/query-927486c9db9989a3187eea43ca6c55c49d1b9fc4c7af6e2f8ca3b198a9341014.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO categories (document_id, name, slug, description, parent_id, sort_order, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "927486c9db9989a3187eea43ca6c55c49d1b9fc4c7af6e2f8ca3b198a9341014" -} diff --git a/.sqlx/query-96d2694f4b75de232afa30f0f9f7dbd1c75d573ea8502e2b748cacdd3fd9bec7.json b/.sqlx/query-96d2694f4b75de232afa30f0f9f7dbd1c75d573ea8502e2b748cacdd3fd9bec7.json deleted file mode 100644 index fa4f0135..00000000 --- a/.sqlx/query-96d2694f4b75de232afa30f0f9f7dbd1c75d573ea8502e2b748cacdd3fd9bec7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_refunds (document_id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, payment_tx_id, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "96d2694f4b75de232afa30f0f9f7dbd1c75d573ea8502e2b748cacdd3fd9bec7" -} diff --git a/.sqlx/query-97829829cd0cb4cba104522aaadacdef534b7bed029494180df79eeb01d11b86.json b/.sqlx/query-97829829cd0cb4cba104522aaadacdef534b7bed029494180df79eeb01d11b86.json new file mode 100644 index 00000000..516bd603 --- /dev/null +++ b/.sqlx/query-97829829cd0cb4cba104522aaadacdef534b7bed029494180df79eeb01d11b86.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO orders (id, user_id, order_no, subtotal, discount_amount, shipping_amount, total_amount, currency, buyer_name, buyer_phone, buyer_email, shipping_address, remark, tax_amount, coupon_id, shipping_address_id, billing_address_id, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", + "describe": { + "columns": [], + "parameters": { + "Right": 20 + }, + "nullable": [] + }, + "hash": "97829829cd0cb4cba104522aaadacdef534b7bed029494180df79eeb01d11b86" +} diff --git a/.sqlx/query-9916f8547bf3da10e652e12e4b1464e5e34b117c8c1cbd319ddf6a271a928839.json b/.sqlx/query-9916f8547bf3da10e652e12e4b1464e5e34b117c8c1cbd319ddf6a271a928839.json deleted file mode 100644 index a27ea674..00000000 --- a/.sqlx/query-9916f8547bf3da10e652e12e4b1464e5e34b117c8c1cbd319ddf6a271a928839.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO posts (document_id, title, slug, content, excerpt, cover_image, status, created_by, updated_by, category_id, published_at, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "9916f8547bf3da10e652e12e4b1464e5e34b117c8c1cbd319ddf6a271a928839" -} diff --git a/.sqlx/query-994ed64b3beb28d532fb3ea1d1c84a074af56e5bb902c0144b68a2fa3caa6fd5.json b/.sqlx/query-994ed64b3beb28d532fb3ea1d1c84a074af56e5bb902c0144b68a2fa3caa6fd5.json deleted file mode 100644 index 23be9e79..00000000 --- a/.sqlx/query-994ed64b3beb28d532fb3ea1d1c84a074af56e5bb902c0144b68a2fa3caa6fd5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO pages (document_id, title, slug, content, blocks, meta_title, meta_description, og_image, template, parent_id, sort_order, status, created_by, updated_by, cover_image, published_at, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", - "describe": { - "columns": [], - "parameters": { - "Right": 19 - }, - "nullable": [] - }, - "hash": "994ed64b3beb28d532fb3ea1d1c84a074af56e5bb902c0144b68a2fa3caa6fd5" -} diff --git a/.sqlx/query-9b99a14fd1f97d84704a85c0ecabc8fe7621e783dc86af4af9ccaccf2263b0b3.json b/.sqlx/query-9b99a14fd1f97d84704a85c0ecabc8fe7621e783dc86af4af9ccaccf2263b0b3.json new file mode 100644 index 00000000..46c8f460 --- /dev/null +++ b/.sqlx/query-9b99a14fd1f97d84704a85c0ecabc8fe7621e783dc86af4af9ccaccf2263b0b3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO roles (id, name, description, is_system, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "9b99a14fd1f97d84704a85c0ecabc8fe7621e783dc86af4af9ccaccf2263b0b3" +} diff --git a/.sqlx/query-9f2b0f4b8dd82f81ffc0802eca98ff38ae754f197a493183d29003daf53c1912.json b/.sqlx/query-9f2b0f4b8dd82f81ffc0802eca98ff38ae754f197a493183d29003daf53c1912.json new file mode 100644 index 00000000..d526dc72 --- /dev/null +++ b/.sqlx/query-9f2b0f4b8dd82f81ffc0802eca98ff38ae754f197a493183d29003daf53c1912.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO orders (id, user_id, order_no, subtotal, discount_amount, shipping_amount, total_amount, currency, buyer_name, buyer_phone, buyer_email, shipping_address, remark, tax_amount, coupon_id, shipping_address_id, billing_address_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", + "describe": { + "columns": [], + "parameters": { + "Right": 19 + }, + "nullable": [] + }, + "hash": "9f2b0f4b8dd82f81ffc0802eca98ff38ae754f197a493183d29003daf53c1912" +} diff --git a/.sqlx/query-a22b7f5f04d6a41911cec171831235a7a0e04bce6209e288ceb8d753eb8a3773.json b/.sqlx/query-a22b7f5f04d6a41911cec171831235a7a0e04bce6209e288ceb8d753eb8a3773.json deleted file mode 100644 index 38d74f3b..00000000 --- a/.sqlx/query-a22b7f5f04d6a41911cec171831235a7a0e04bce6209e288ceb8d753eb8a3773.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO reusable_blocks (document_id, name, block_type, content, description, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "a22b7f5f04d6a41911cec171831235a7a0e04bce6209e288ceb8d753eb8a3773" -} diff --git a/.sqlx/query-a499c71b4a7b0936e4661b4bcbd6c12935fc8e3650bbd6aa394a3564ce07076f.json b/.sqlx/query-a499c71b4a7b0936e4661b4bcbd6c12935fc8e3650bbd6aa394a3564ce07076f.json deleted file mode 100644 index 64d4f860..00000000 --- a/.sqlx/query-a499c71b4a7b0936e4661b4bcbd6c12935fc8e3650bbd6aa394a3564ce07076f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO order_items (document_id, order_id, product_id, variant_id, title, description, sku, unit_price, quantity, subtotal, tax_amount, cover_url, attributes, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", - "describe": { - "columns": [], - "parameters": { - "Right": 15 - }, - "nullable": [] - }, - "hash": "a499c71b4a7b0936e4661b4bcbd6c12935fc8e3650bbd6aa394a3564ce07076f" -} diff --git a/.sqlx/query-a59c89fc04a16acca91715ef62fc747cb9685f805763b8628f8a7433bddbd17e.json b/.sqlx/query-a59c89fc04a16acca91715ef62fc747cb9685f805763b8628f8a7433bddbd17e.json new file mode 100644 index 00000000..53a59767 --- /dev/null +++ b/.sqlx/query-a59c89fc04a16acca91715ef62fc747cb9685f805763b8628f8a7433bddbd17e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO content_revisions (id, content_type, record_id, revision_number, snapshot, created_by, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "a59c89fc04a16acca91715ef62fc747cb9685f805763b8628f8a7433bddbd17e" +} diff --git a/.sqlx/query-a9127f524b96e405be2128d8bcd9563530efe3a3762bc04d8393e0302f5d1741.json b/.sqlx/query-a9127f524b96e405be2128d8bcd9563530efe3a3762bc04d8393e0302f5d1741.json deleted file mode 100644 index 62018e37..00000000 --- a/.sqlx/query-a9127f524b96e405be2128d8bcd9563530efe3a3762bc04d8393e0302f5d1741.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO oauth_accounts (document_id, user_id, provider, provider_user_id, email, display_name, avatar_url, access_token, refresh_token, token_expires_at, profile, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "a9127f524b96e405be2128d8bcd9563530efe3a3762bc04d8393e0302f5d1741" -} diff --git a/.sqlx/query-a9f3579fc327fad0377051402d61272644c6d0521b0217d32199150c7b5d84be.json b/.sqlx/query-a9f3579fc327fad0377051402d61272644c6d0521b0217d32199150c7b5d84be.json new file mode 100644 index 00000000..1ac6ca41 --- /dev/null +++ b/.sqlx/query-a9f3579fc327fad0377051402d61272644c6d0521b0217d32199150c7b5d84be.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO comments (id, post_id, created_by, updated_by, nickname, email, content, parent_id, created_at, updated_at, status, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "a9f3579fc327fad0377051402d61272644c6d0521b0217d32199150c7b5d84be" +} diff --git a/.sqlx/query-acdcafc1ae4bf2bc137e9c0b8468f9c9b3e9d8b37e8db0e0bacc44ec2aea944a.json b/.sqlx/query-acdcafc1ae4bf2bc137e9c0b8468f9c9b3e9d8b37e8db0e0bacc44ec2aea944a.json deleted file mode 100644 index 8170ca50..00000000 --- a/.sqlx/query-acdcafc1ae4bf2bc137e9c0b8468f9c9b3e9d8b37e8db0e0bacc44ec2aea944a.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_refunds (document_id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, payment_tx_id, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "acdcafc1ae4bf2bc137e9c0b8468f9c9b3e9d8b37e8db0e0bacc44ec2aea944a" -} diff --git a/.sqlx/query-b48d717f1f3713fcc2d11baa83d8ce24d1b84cb148c87fdcbda5f69e9c267743.json b/.sqlx/query-b48d717f1f3713fcc2d11baa83d8ce24d1b84cb148c87fdcbda5f69e9c267743.json new file mode 100644 index 00000000..dd8ff85a --- /dev/null +++ b/.sqlx/query-b48d717f1f3713fcc2d11baa83d8ce24d1b84cb148c87fdcbda5f69e9c267743.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO wallet_outbox (id, user_id, currency, amount, entry_type, tx_type, transaction_no, reference_type, reference_id, metadata, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "b48d717f1f3713fcc2d11baa83d8ce24d1b84cb148c87fdcbda5f69e9c267743" +} diff --git a/.sqlx/query-b69d3a2b6e56a6a2b54664758beaeecaef1b744ac6ad762fd8f95fca78a076be.json b/.sqlx/query-b69d3a2b6e56a6a2b54664758beaeecaef1b744ac6ad762fd8f95fca78a076be.json deleted file mode 100644 index f0f60083..00000000 --- a/.sqlx/query-b69d3a2b6e56a6a2b54664758beaeecaef1b744ac6ad762fd8f95fca78a076be.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO permissions (document_id, role_id, action, subject, fields, conditions, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "b69d3a2b6e56a6a2b54664758beaeecaef1b744ac6ad762fd8f95fca78a076be" -} diff --git a/.sqlx/query-b76f4e37b51afe15519f57c4046282090c0b874557bf646ef9b2e7c623c28cde.json b/.sqlx/query-b76f4e37b51afe15519f57c4046282090c0b874557bf646ef9b2e7c623c28cde.json new file mode 100644 index 00000000..98172c80 --- /dev/null +++ b/.sqlx/query-b76f4e37b51afe15519f57c4046282090c0b874557bf646ef9b2e7c623c28cde.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO cart_items (id, user_id, product_id, variant_id, quantity, attributes, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + "describe": { + "columns": [], + "parameters": { + "Right": 9 + }, + "nullable": [] + }, + "hash": "b76f4e37b51afe15519f57c4046282090c0b874557bf646ef9b2e7c623c28cde" +} diff --git a/.sqlx/query-b8551631f34baaa46ef4ac31286c8adb077ae16fc31ce2f9c972f679593eac3d.json b/.sqlx/query-b8551631f34baaa46ef4ac31286c8adb077ae16fc31ce2f9c972f679593eac3d.json new file mode 100644 index 00000000..65927b5e --- /dev/null +++ b/.sqlx/query-b8551631f34baaa46ef4ac31286c8adb077ae16fc31ce2f9c972f679593eac3d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO categories (id, name, slug, description, parent_id, sort_order, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "b8551631f34baaa46ef4ac31286c8adb077ae16fc31ce2f9c972f679593eac3d" +} diff --git a/.sqlx/query-b8c1052e2e35b3c762d36e1123d9b0ec18617974eea17f926a4ea4064549854f.json b/.sqlx/query-b8c1052e2e35b3c762d36e1123d9b0ec18617974eea17f926a4ea4064549854f.json new file mode 100644 index 00000000..5109baaa --- /dev/null +++ b/.sqlx/query-b8c1052e2e35b3c762d36e1123d9b0ec18617974eea17f926a4ea4064549854f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_orders (id, user_id, order_id, title, amount, currency, channel_id, provider, reference_type, reference_id, return_url, idempotency_key, client_ip, client_language, client_country, client_user_agent, channel_selected_by, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21)", + "describe": { + "columns": [], + "parameters": { + "Right": 21 + }, + "nullable": [] + }, + "hash": "b8c1052e2e35b3c762d36e1123d9b0ec18617974eea17f926a4ea4064549854f" +} diff --git a/.sqlx/query-b8c239c447aaf669ea6c83ddf842088b03fbdb7843bd7868c4cfcc0381112922.json b/.sqlx/query-b8c239c447aaf669ea6c83ddf842088b03fbdb7843bd7868c4cfcc0381112922.json deleted file mode 100644 index 678c3ca4..00000000 --- a/.sqlx/query-b8c239c447aaf669ea6c83ddf842088b03fbdb7843bd7868c4cfcc0381112922.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO products (document_id, category_id, title, description, cover_url, product_type, fulfillment_type, delivery_hook, weight, price, currency, attributes, sort_order, slug, content, image_ids, original_price, specs, unit, min_purchase, max_purchase, virtual_sales, meta_title, meta_description, stock, cost_price, sale_price, has_variants, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31)", - "describe": { - "columns": [], - "parameters": { - "Right": 31 - }, - "nullable": [] - }, - "hash": "b8c239c447aaf669ea6c83ddf842088b03fbdb7843bd7868c4cfcc0381112922" -} diff --git a/.sqlx/query-b972949428e68d37ad3075ff6c15d470924d68eccf4acfe0ca71907d46f7ea87.json b/.sqlx/query-b972949428e68d37ad3075ff6c15d470924d68eccf4acfe0ca71907d46f7ea87.json new file mode 100644 index 00000000..70c9544e --- /dev/null +++ b/.sqlx/query-b972949428e68d37ad3075ff6c15d470924d68eccf4acfe0ca71907d46f7ea87.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO webhook_subscriptions (id, url, secret, events, enabled, description, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "b972949428e68d37ad3075ff6c15d470924d68eccf4acfe0ca71907d46f7ea87" +} diff --git a/.sqlx/query-b9a06aa2f8b4e7fd69470ffc826540af18caa9d9f78e788054056e1c4ef939af.json b/.sqlx/query-b9a06aa2f8b4e7fd69470ffc826540af18caa9d9f78e788054056e1c4ef939af.json new file mode 100644 index 00000000..2f2ac5e9 --- /dev/null +++ b/.sqlx/query-b9a06aa2f8b4e7fd69470ffc826540af18caa9d9f78e788054056e1c4ef939af.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO categories (id, name, slug, description, parent_id, sort_order, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "b9a06aa2f8b4e7fd69470ffc826540af18caa9d9f78e788054056e1c4ef939af" +} diff --git a/.sqlx/query-bb350274e9cf006caa1d3f0f8d1db7b56298aecfe3f00ec1c3731ed65bd4a8fa.json b/.sqlx/query-bb350274e9cf006caa1d3f0f8d1db7b56298aecfe3f00ec1c3731ed65bd4a8fa.json deleted file mode 100644 index b33e3b12..00000000 --- a/.sqlx/query-bb350274e9cf006caa1d3f0f8d1db7b56298aecfe3f00ec1c3731ed65bd4a8fa.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_channels (document_id, provider, name, is_live, credentials, webhook_secret, settings, is_active, sort_order, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "bb350274e9cf006caa1d3f0f8d1db7b56298aecfe3f00ec1c3731ed65bd4a8fa" -} diff --git a/.sqlx/query-bccaa81b9d8979c9313a585be1ab21a2a87033ee69ee5a5e9498062e4e49722c.json b/.sqlx/query-bccaa81b9d8979c9313a585be1ab21a2a87033ee69ee5a5e9498062e4e49722c.json new file mode 100644 index 00000000..0481cbb3 --- /dev/null +++ b/.sqlx/query-bccaa81b9d8979c9313a585be1ab21a2a87033ee69ee5a5e9498062e4e49722c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tags (id, name, slug, created_by, updated_by, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "bccaa81b9d8979c9313a585be1ab21a2a87033ee69ee5a5e9498062e4e49722c" +} diff --git a/.sqlx/query-be4324ba43c80e343b654c60768b612494c1374783989d5a1353a99cb39cbe90.json b/.sqlx/query-be4324ba43c80e343b654c60768b612494c1374783989d5a1353a99cb39cbe90.json new file mode 100644 index 00000000..ba2e8223 --- /dev/null +++ b/.sqlx/query-be4324ba43c80e343b654c60768b612494c1374783989d5a1353a99cb39cbe90.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_channels (id, provider, name, is_live, credentials, webhook_secret, settings, is_active, sort_order, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "be4324ba43c80e343b654c60768b612494c1374783989d5a1353a99cb39cbe90" +} diff --git a/.sqlx/query-bed615f4573501020fa03300df7cda1cff0a844e474bc8e2df67ec60d7c8c9a9.json b/.sqlx/query-bed615f4573501020fa03300df7cda1cff0a844e474bc8e2df67ec60d7c8c9a9.json new file mode 100644 index 00000000..ad0408c8 --- /dev/null +++ b/.sqlx/query-bed615f4573501020fa03300df7cda1cff0a844e474bc8e2df67ec60d7c8c9a9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM api_tokens WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "bed615f4573501020fa03300df7cda1cff0a844e474bc8e2df67ec60d7c8c9a9" +} diff --git a/.sqlx/query-c14c06e841097b2f930e247ef56abb752dec62b87556f0084b8a7d826b9a32c4.json b/.sqlx/query-c14c06e841097b2f930e247ef56abb752dec62b87556f0084b8a7d826b9a32c4.json new file mode 100644 index 00000000..69752c98 --- /dev/null +++ b/.sqlx/query-c14c06e841097b2f930e247ef56abb752dec62b87556f0084b8a7d826b9a32c4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM roles WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "c14c06e841097b2f930e247ef56abb752dec62b87556f0084b8a7d826b9a32c4" +} diff --git a/.sqlx/query-c60e6d14f4fbfb5552cd51de32444b3a1e1617da56f9929ba3a1b05e2d9b0815.json b/.sqlx/query-c60e6d14f4fbfb5552cd51de32444b3a1e1617da56f9929ba3a1b05e2d9b0815.json deleted file mode 100644 index d4f433a7..00000000 --- a/.sqlx/query-c60e6d14f4fbfb5552cd51de32444b3a1e1617da56f9929ba3a1b05e2d9b0815.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO users (document_id, username, created_at, updated_at, role, status, registered_via, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "c60e6d14f4fbfb5552cd51de32444b3a1e1617da56f9929ba3a1b05e2d9b0815" -} diff --git a/.sqlx/query-ca026c702e1f298987fddaeef413f80ef1145b31d56d43bb780b8db500f4c6c2.json b/.sqlx/query-ca026c702e1f298987fddaeef413f80ef1145b31d56d43bb780b8db500f4c6c2.json new file mode 100644 index 00000000..b13f3af1 --- /dev/null +++ b/.sqlx/query-ca026c702e1f298987fddaeef413f80ef1145b31d56d43bb780b8db500f4c6c2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO user_addresses (id, user_id, label, recipient_name, phone, country, province, city, district, address_line1, address_line2, postal_code, is_default, address_type, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", + "describe": { + "columns": [], + "parameters": { + "Right": 17 + }, + "nullable": [] + }, + "hash": "ca026c702e1f298987fddaeef413f80ef1145b31d56d43bb780b8db500f4c6c2" +} diff --git a/.sqlx/query-ccf6d9edfc09aab5a4e082468c3258aabd4f0688856614fceb7eead0a17114ce.json b/.sqlx/query-ccf6d9edfc09aab5a4e082468c3258aabd4f0688856614fceb7eead0a17114ce.json new file mode 100644 index 00000000..27cdbe81 --- /dev/null +++ b/.sqlx/query-ccf6d9edfc09aab5a4e082468c3258aabd4f0688856614fceb7eead0a17114ce.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO product_variants (id, product_id, sku, title, price, original_price, stock, attributes, sort_order, is_active, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "ccf6d9edfc09aab5a4e082468c3258aabd4f0688856614fceb7eead0a17114ce" +} diff --git a/.sqlx/query-cd430e82ffb58f8831d5720503a044af92977a83f9f5b5adccd2c8b163926900.json b/.sqlx/query-cd430e82ffb58f8831d5720503a044af92977a83f9f5b5adccd2c8b163926900.json new file mode 100644 index 00000000..2e22a415 --- /dev/null +++ b/.sqlx/query-cd430e82ffb58f8831d5720503a044af92977a83f9f5b5adccd2c8b163926900.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO products (id, category_id, title, description, cover_url, product_type, fulfillment_type, delivery_hook, weight, price, currency, attributes, sort_order, slug, content, image_ids, original_price, specs, unit, min_purchase, max_purchase, virtual_sales, meta_title, meta_description, stock, cost_price, sale_price, has_variants, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31)", + "describe": { + "columns": [], + "parameters": { + "Right": 31 + }, + "nullable": [] + }, + "hash": "cd430e82ffb58f8831d5720503a044af92977a83f9f5b5adccd2c8b163926900" +} diff --git a/.sqlx/query-cdaf7a8cb2657d0c7cd05d9219f6be599279b1adc3978387c42eca5eb7fd23ad.json b/.sqlx/query-cdaf7a8cb2657d0c7cd05d9219f6be599279b1adc3978387c42eca5eb7fd23ad.json new file mode 100644 index 00000000..ed2d6a3e --- /dev/null +++ b/.sqlx/query-cdaf7a8cb2657d0c7cd05d9219f6be599279b1adc3978387c42eca5eb7fd23ad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_refunds (id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, payment_tx_id, metadata, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "cdaf7a8cb2657d0c7cd05d9219f6be599279b1adc3978387c42eca5eb7fd23ad" +} diff --git a/.sqlx/query-ce75e804f68650c3f9eb766a5c8fa9553d710aaeccc27b0bf2c52e9e9fc17bbd.json b/.sqlx/query-ce75e804f68650c3f9eb766a5c8fa9553d710aaeccc27b0bf2c52e9e9fc17bbd.json deleted file mode 100644 index 25cf59cf..00000000 --- a/.sqlx/query-ce75e804f68650c3f9eb766a5c8fa9553d710aaeccc27b0bf2c52e9e9fc17bbd.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO product_variants (document_id, product_id, sku, title, price, original_price, stock, attributes, sort_order, is_active, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "ce75e804f68650c3f9eb766a5c8fa9553d710aaeccc27b0bf2c52e9e9fc17bbd" -} diff --git a/.sqlx/query-cff66d74ea51fad9a845b398e6832a1d629cb5e63931b2ef9a98739a71eab951.json b/.sqlx/query-cff66d74ea51fad9a845b398e6832a1d629cb5e63931b2ef9a98739a71eab951.json deleted file mode 100644 index 73af5909..00000000 --- a/.sqlx/query-cff66d74ea51fad9a845b398e6832a1d629cb5e63931b2ef9a98739a71eab951.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO product_variants (document_id, product_id, sku, title, price, original_price, stock, attributes, sort_order, is_active, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "cff66d74ea51fad9a845b398e6832a1d629cb5e63931b2ef9a98739a71eab951" -} diff --git a/.sqlx/query-d0f119cc1830f2196da20e43ad68bff5ce0c60e5aaf4be088a21dad6b952d7be.json b/.sqlx/query-d0f119cc1830f2196da20e43ad68bff5ce0c60e5aaf4be088a21dad6b952d7be.json deleted file mode 100644 index a325ab25..00000000 --- a/.sqlx/query-d0f119cc1830f2196da20e43ad68bff5ce0c60e5aaf4be088a21dad6b952d7be.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO pages (document_id, title, slug, content, blocks, meta_title, meta_description, og_image, template, parent_id, sort_order, status, created_by, updated_by, cover_image, published_at, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", - "describe": { - "columns": [], - "parameters": { - "Right": 18 - }, - "nullable": [] - }, - "hash": "d0f119cc1830f2196da20e43ad68bff5ce0c60e5aaf4be088a21dad6b952d7be" -} diff --git a/.sqlx/query-d3efddaf5242931a09636e71de804fd9b32b3fdfdbee25f64583e14ae20aad60.json b/.sqlx/query-d3efddaf5242931a09636e71de804fd9b32b3fdfdbee25f64583e14ae20aad60.json deleted file mode 100644 index 17630dac..00000000 --- a/.sqlx/query-d3efddaf5242931a09636e71de804fd9b32b3fdfdbee25f64583e14ae20aad60.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO products (document_id, category_id, title, description, cover_url, product_type, fulfillment_type, delivery_hook, weight, price, currency, attributes, sort_order, slug, content, image_ids, original_price, specs, unit, min_purchase, max_purchase, virtual_sales, meta_title, meta_description, stock, cost_price, sale_price, has_variants, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30)", - "describe": { - "columns": [], - "parameters": { - "Right": 30 - }, - "nullable": [] - }, - "hash": "d3efddaf5242931a09636e71de804fd9b32b3fdfdbee25f64583e14ae20aad60" -} diff --git a/.sqlx/query-d887b374016efd58de5881a05c9a91fa2fd60313ffd0a56312b0bb3b176d5f27.json b/.sqlx/query-d887b374016efd58de5881a05c9a91fa2fd60313ffd0a56312b0bb3b176d5f27.json deleted file mode 100644 index 50757f7e..00000000 --- a/.sqlx/query-d887b374016efd58de5881a05c9a91fa2fd60313ffd0a56312b0bb3b176d5f27.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO password_reset_tokens (document_id, user_id, token, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "d887b374016efd58de5881a05c9a91fa2fd60313ffd0a56312b0bb3b176d5f27" -} diff --git a/.sqlx/query-db043f4ef370f2636f8ac7441f328ab19e1242864d21955ba1878a14ab6ea5ae.json b/.sqlx/query-db043f4ef370f2636f8ac7441f328ab19e1242864d21955ba1878a14ab6ea5ae.json deleted file mode 100644 index 47c0d685..00000000 --- a/.sqlx/query-db043f4ef370f2636f8ac7441f328ab19e1242864d21955ba1878a14ab6ea5ae.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO webhook_subscriptions (document_id, url, secret, events, enabled, description, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - "describe": { - "columns": [], - "parameters": { - "Right": 9 - }, - "nullable": [] - }, - "hash": "db043f4ef370f2636f8ac7441f328ab19e1242864d21955ba1878a14ab6ea5ae" -} diff --git a/.sqlx/query-dc13649fc9396997f38902632b95657927befc68b0a5f4a3c5064e7e356500bb.json b/.sqlx/query-dc13649fc9396997f38902632b95657927befc68b0a5f4a3c5064e7e356500bb.json deleted file mode 100644 index 343dd147..00000000 --- a/.sqlx/query-dc13649fc9396997f38902632b95657927befc68b0a5f4a3c5064e7e356500bb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM tenants WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "dc13649fc9396997f38902632b95657927befc68b0a5f4a3c5064e7e356500bb" -} diff --git a/.sqlx/query-dc51c24ab845e4bfac57e5eb1865c4d34c7772b2c1455ec1ac6f22073d28797d.json b/.sqlx/query-dc51c24ab845e4bfac57e5eb1865c4d34c7772b2c1455ec1ac6f22073d28797d.json deleted file mode 100644 index e58fce85..00000000 --- a/.sqlx/query-dc51c24ab845e4bfac57e5eb1865c4d34c7772b2c1455ec1ac6f22073d28797d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO payment_transactions (document_id, payment_order_id, order_id, user_id, tx_type, amount, currency, provider_tx_id, status, raw_payload, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "dc51c24ab845e4bfac57e5eb1865c4d34c7772b2c1455ec1ac6f22073d28797d" -} diff --git a/.sqlx/query-e09f07a3f57f2417cec71e94f856f2ff4a9e570df4ca962178674b8bda40072c.json b/.sqlx/query-e09f07a3f57f2417cec71e94f856f2ff4a9e570df4ca962178674b8bda40072c.json deleted file mode 100644 index 398bc0a9..00000000 --- a/.sqlx/query-e09f07a3f57f2417cec71e94f856f2ff4a9e570df4ca962178674b8bda40072c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO oauth_states (document_id, provider, code_verifier, user_id, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "e09f07a3f57f2417cec71e94f856f2ff4a9e570df4ca962178674b8bda40072c" -} diff --git a/.sqlx/query-e1764e3c39ab4ce487084f4129d3c4ad73923e8d73eb6af6f1bbe67df7c837bb.json b/.sqlx/query-e1764e3c39ab4ce487084f4129d3c4ad73923e8d73eb6af6f1bbe67df7c837bb.json deleted file mode 100644 index 493a5c2d..00000000 --- a/.sqlx/query-e1764e3c39ab4ce487084f4129d3c4ad73923e8d73eb6af6f1bbe67df7c837bb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO email_verification_tokens (document_id, user_id, token, email, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "e1764e3c39ab4ce487084f4129d3c4ad73923e8d73eb6af6f1bbe67df7c837bb" -} diff --git a/.sqlx/query-e35f9b20d7340b4c59889732d552af772028c349bd46e073b15a6055364b059f.json b/.sqlx/query-e35f9b20d7340b4c59889732d552af772028c349bd46e073b15a6055364b059f.json new file mode 100644 index 00000000..9c553404 --- /dev/null +++ b/.sqlx/query-e35f9b20d7340b4c59889732d552af772028c349bd46e073b15a6055364b059f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users (id, username, created_at, updated_at, role, status, registered_via, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + "describe": { + "columns": [], + "parameters": { + "Right": 8 + }, + "nullable": [] + }, + "hash": "e35f9b20d7340b4c59889732d552af772028c349bd46e073b15a6055364b059f" +} diff --git a/.sqlx/query-e38cf91855fc4c311b74f695bf6f06900f9650c01673379f6eb26e0b42df1487.json b/.sqlx/query-e38cf91855fc4c311b74f695bf6f06900f9650c01673379f6eb26e0b42df1487.json new file mode 100644 index 00000000..6f5e2f9c --- /dev/null +++ b/.sqlx/query-e38cf91855fc4c311b74f695bf6f06900f9650c01673379f6eb26e0b42df1487.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO payment_refunds (id, payment_order_id, order_id, user_id, amount, currency, reason, provider_refund_id, status, metadata, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "e38cf91855fc4c311b74f695bf6f06900f9650c01673379f6eb26e0b42df1487" +} diff --git a/.sqlx/query-e42e6f66a9e9eaf49e3d2b554ed0069bcfbfe61ea8befde895bae56980e4f778.json b/.sqlx/query-e42e6f66a9e9eaf49e3d2b554ed0069bcfbfe61ea8befde895bae56980e4f778.json new file mode 100644 index 00000000..8cee7e59 --- /dev/null +++ b/.sqlx/query-e42e6f66a9e9eaf49e3d2b554ed0069bcfbfe61ea8befde895bae56980e4f778.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO sms_codes (id, phone, code, purpose, expires_at, ip_address, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "e42e6f66a9e9eaf49e3d2b554ed0069bcfbfe61ea8befde895bae56980e4f778" +} diff --git a/.sqlx/query-e4edc339cde1b12bdbe5e8cc761d49e37541daf536c2ce5b13d37bf7594dd208.json b/.sqlx/query-e4edc339cde1b12bdbe5e8cc761d49e37541daf536c2ce5b13d37bf7594dd208.json new file mode 100644 index 00000000..799a262f --- /dev/null +++ b/.sqlx/query-e4edc339cde1b12bdbe5e8cc761d49e37541daf536c2ce5b13d37bf7594dd208.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO password_reset_tokens (id, user_id, token, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "e4edc339cde1b12bdbe5e8cc761d49e37541daf536c2ce5b13d37bf7594dd208" +} diff --git a/.sqlx/query-e7ec039830a9766566351f7ca540d88f603d86cc04cb4b55c61192fa057dbb94.json b/.sqlx/query-e7ec039830a9766566351f7ca540d88f603d86cc04cb4b55c61192fa057dbb94.json deleted file mode 100644 index ab29927a..00000000 --- a/.sqlx/query-e7ec039830a9766566351f7ca540d88f603d86cc04cb4b55c61192fa057dbb94.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM users WHERE document_id = ?1", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "e7ec039830a9766566351f7ca540d88f603d86cc04cb4b55c61192fa057dbb94" -} diff --git a/.sqlx/query-e881e1880fced15594a1004a1b5ba8e25efcc061146be913ea01bc7b1dd6b1f1.json b/.sqlx/query-e881e1880fced15594a1004a1b5ba8e25efcc061146be913ea01bc7b1dd6b1f1.json deleted file mode 100644 index 8d36d00c..00000000 --- a/.sqlx/query-e881e1880fced15594a1004a1b5ba8e25efcc061146be913ea01bc7b1dd6b1f1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO audit_log (document_id, actor_id, actor_role, action, subject, subject_id, detail, ip_address, user_agent, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - "describe": { - "columns": [], - "parameters": { - "Right": 11 - }, - "nullable": [] - }, - "hash": "e881e1880fced15594a1004a1b5ba8e25efcc061146be913ea01bc7b1dd6b1f1" -} diff --git a/.sqlx/query-eb303f6bfba8d1831c84691aba50ec40bb2f1cce89e3da14702bb25700cd6688.json b/.sqlx/query-eb303f6bfba8d1831c84691aba50ec40bb2f1cce89e3da14702bb25700cd6688.json deleted file mode 100644 index 38120695..00000000 --- a/.sqlx/query-eb303f6bfba8d1831c84691aba50ec40bb2f1cce89e3da14702bb25700cd6688.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO media (document_id, user_id, filename, filepath, mimetype, size, width, height, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "eb303f6bfba8d1831c84691aba50ec40bb2f1cce89e3da14702bb25700cd6688" -} diff --git a/.sqlx/query-f2afec774a9737e8fccc17f3470028846c2a16e8be98006ec369bdd3e0ddf015.json b/.sqlx/query-f2afec774a9737e8fccc17f3470028846c2a16e8be98006ec369bdd3e0ddf015.json deleted file mode 100644 index 6dc03af2..00000000 --- a/.sqlx/query-f2afec774a9737e8fccc17f3470028846c2a16e8be98006ec369bdd3e0ddf015.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO orders (document_id, user_id, order_no, subtotal, discount_amount, shipping_amount, total_amount, currency, buyer_name, buyer_phone, buyer_email, shipping_address, remark, tax_amount, coupon_id, shipping_address_id, billing_address_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", - "describe": { - "columns": [], - "parameters": { - "Right": 19 - }, - "nullable": [] - }, - "hash": "f2afec774a9737e8fccc17f3470028846c2a16e8be98006ec369bdd3e0ddf015" -} diff --git a/.sqlx/query-f3be8b5cb0b4143655c4b57479ce429c06452429f234660b1e85e6cc38fc51aa.json b/.sqlx/query-f3be8b5cb0b4143655c4b57479ce429c06452429f234660b1e85e6cc38fc51aa.json new file mode 100644 index 00000000..28ab0e13 --- /dev/null +++ b/.sqlx/query-f3be8b5cb0b4143655c4b57479ce429c06452429f234660b1e85e6cc38fc51aa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM cart_items WHERE id = ?1 AND tenant_id = ?2", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "f3be8b5cb0b4143655c4b57479ce429c06452429f234660b1e85e6cc38fc51aa" +} diff --git a/.sqlx/query-f52733df58c6572f8a76d0119bb4d856f0871a0f8a8dd2c5e8a17b53f8b8c867.json b/.sqlx/query-f52733df58c6572f8a76d0119bb4d856f0871a0f8a8dd2c5e8a17b53f8b8c867.json deleted file mode 100644 index 0668e639..00000000 --- a/.sqlx/query-f52733df58c6572f8a76d0119bb4d856f0871a0f8a8dd2c5e8a17b53f8b8c867.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO wallet_outbox (document_id, user_id, currency, amount, entry_type, tx_type, transaction_no, reference_type, reference_id, metadata, status, created_at, updated_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - "describe": { - "columns": [], - "parameters": { - "Right": 14 - }, - "nullable": [] - }, - "hash": "f52733df58c6572f8a76d0119bb4d856f0871a0f8a8dd2c5e8a17b53f8b8c867" -} diff --git a/.sqlx/query-f5d65300def492fc78e23578e7e2b0b0b8b978b47085031789ad5a742cfcc4ba.json b/.sqlx/query-f5d65300def492fc78e23578e7e2b0b0b8b978b47085031789ad5a742cfcc4ba.json deleted file mode 100644 index 85e864c3..00000000 --- a/.sqlx/query-f5d65300def492fc78e23578e7e2b0b0b8b978b47085031789ad5a742cfcc4ba.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO webhook_subscriptions (document_id, url, secret, events, enabled, description, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "f5d65300def492fc78e23578e7e2b0b0b8b978b47085031789ad5a742cfcc4ba" -} diff --git a/.sqlx/query-f6843d1c158437e04709ff74f1c92235453e78aea05731513fbe11f54dede031.json b/.sqlx/query-f6843d1c158437e04709ff74f1c92235453e78aea05731513fbe11f54dede031.json deleted file mode 100644 index 734ad60b..00000000 --- a/.sqlx/query-f6843d1c158437e04709ff74f1c92235453e78aea05731513fbe11f54dede031.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM users WHERE document_id = ?1 AND tenant_id = ?2", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "f6843d1c158437e04709ff74f1c92235453e78aea05731513fbe11f54dede031" -} diff --git a/.sqlx/query-f758e4d5a6991e6e974b2e7a6bce796a81107e90c4c38aea79b5f71102713cde.json b/.sqlx/query-f758e4d5a6991e6e974b2e7a6bce796a81107e90c4c38aea79b5f71102713cde.json deleted file mode 100644 index ac257c3a..00000000 --- a/.sqlx/query-f758e4d5a6991e6e974b2e7a6bce796a81107e90c4c38aea79b5f71102713cde.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO roles (document_id, name, description, is_system, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - "describe": { - "columns": [], - "parameters": { - "Right": 6 - }, - "nullable": [] - }, - "hash": "f758e4d5a6991e6e974b2e7a6bce796a81107e90c4c38aea79b5f71102713cde" -} diff --git a/.sqlx/query-f8207c8b05da03ca3f2c792ba7019ba207e7da6c97254a23c2f5e2773ff32a13.json b/.sqlx/query-f8207c8b05da03ca3f2c792ba7019ba207e7da6c97254a23c2f5e2773ff32a13.json new file mode 100644 index 00000000..cd77dda5 --- /dev/null +++ b/.sqlx/query-f8207c8b05da03ca3f2c792ba7019ba207e7da6c97254a23c2f5e2773ff32a13.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO tenants (id, name, domain, config, status, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "f8207c8b05da03ca3f2c792ba7019ba207e7da6c97254a23c2f5e2773ff32a13" +} diff --git a/.sqlx/query-f9f73cc5a5be6b4c2c69d0250ad10edf13203ae3dabc6053f4b8dd9047417c17.json b/.sqlx/query-f9f73cc5a5be6b4c2c69d0250ad10edf13203ae3dabc6053f4b8dd9047417c17.json new file mode 100644 index 00000000..c5c339f2 --- /dev/null +++ b/.sqlx/query-f9f73cc5a5be6b4c2c69d0250ad10edf13203ae3dabc6053f4b8dd9047417c17.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO products (id, category_id, title, description, cover_url, product_type, fulfillment_type, delivery_hook, weight, price, currency, attributes, sort_order, slug, content, image_ids, original_price, specs, unit, min_purchase, max_purchase, virtual_sales, meta_title, meta_description, stock, cost_price, sale_price, has_variants, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30)", + "describe": { + "columns": [], + "parameters": { + "Right": 30 + }, + "nullable": [] + }, + "hash": "f9f73cc5a5be6b4c2c69d0250ad10edf13203ae3dabc6053f4b8dd9047417c17" +} diff --git a/.sqlx/query-f9fcacf81191ee798e000cd9e122d98b16d21f71ce69f379a9dc74bf85354f52.json b/.sqlx/query-f9fcacf81191ee798e000cd9e122d98b16d21f71ce69f379a9dc74bf85354f52.json new file mode 100644 index 00000000..9594da6f --- /dev/null +++ b/.sqlx/query-f9fcacf81191ee798e000cd9e122d98b16d21f71ce69f379a9dc74bf85354f52.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM tenants WHERE id = ?1", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f9fcacf81191ee798e000cd9e122d98b16d21f71ce69f379a9dc74bf85354f52" +} diff --git a/.sqlx/query-fb111827f5db51855e8d67b2c72dc7078d390a041bb999e757b1cb7e78dc9de8.json b/.sqlx/query-fb111827f5db51855e8d67b2c72dc7078d390a041bb999e757b1cb7e78dc9de8.json deleted file mode 100644 index da886707..00000000 --- a/.sqlx/query-fb111827f5db51855e8d67b2c72dc7078d390a041bb999e757b1cb7e78dc9de8.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM cart_items WHERE document_id = ?1 AND tenant_id = ?2", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "fb111827f5db51855e8d67b2c72dc7078d390a041bb999e757b1cb7e78dc9de8" -} diff --git a/.sqlx/query-fb74bff60db4204bb7b88d4825254d76551d25a16eebec367d92f5ee987d2ade.json b/.sqlx/query-fb74bff60db4204bb7b88d4825254d76551d25a16eebec367d92f5ee987d2ade.json new file mode 100644 index 00000000..f8c51f16 --- /dev/null +++ b/.sqlx/query-fb74bff60db4204bb7b88d4825254d76551d25a16eebec367d92f5ee987d2ade.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO audit_log (id, actor_id, actor_role, action, subject, subject_id, detail, ip_address, user_agent, created_at, tenant_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "fb74bff60db4204bb7b88d4825254d76551d25a16eebec367d92f5ee987d2ade" +} diff --git a/.sqlx/query-fbeb58fe9142da315b1101249e6d0c83f4e7f42e239c67fb2780a2791beb849d.json b/.sqlx/query-fbeb58fe9142da315b1101249e6d0c83f4e7f42e239c67fb2780a2791beb849d.json new file mode 100644 index 00000000..7d0b7e5c --- /dev/null +++ b/.sqlx/query-fbeb58fe9142da315b1101249e6d0c83f4e7f42e239c67fb2780a2791beb849d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO email_verification_tokens (id, user_id, token, email, expires_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "fbeb58fe9142da315b1101249e6d0c83f4e7f42e239c67fb2780a2791beb849d" +} diff --git a/.sqlx/query-fd8754bc010176d57db6441658aa832f1fb464ae29bac5f0623b4f1766b719c2.json b/.sqlx/query-fd8754bc010176d57db6441658aa832f1fb464ae29bac5f0623b4f1766b719c2.json deleted file mode 100644 index bcc8933c..00000000 --- a/.sqlx/query-fd8754bc010176d57db6441658aa832f1fb464ae29bac5f0623b4f1766b719c2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO categories (document_id, name, slug, description, parent_id, sort_order, created_by, updated_by, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "fd8754bc010176d57db6441658aa832f1fb464ae29bac5f0623b4f1766b719c2" -} diff --git a/.sqlx/query-fdcb5e63baa1eead3f4d91138dcb3062128639546a2e1bac4d8f6fccf6e16bc1.json b/.sqlx/query-fdcb5e63baa1eead3f4d91138dcb3062128639546a2e1bac4d8f6fccf6e16bc1.json deleted file mode 100644 index 5a9895d7..00000000 --- a/.sqlx/query-fdcb5e63baa1eead3f4d91138dcb3062128639546a2e1bac4d8f6fccf6e16bc1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO cart_items (document_id, user_id, product_id, variant_id, quantity, attributes, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", - "describe": { - "columns": [], - "parameters": { - "Right": 8 - }, - "nullable": [] - }, - "hash": "fdcb5e63baa1eead3f4d91138dcb3062128639546a2e1bac4d8f6fccf6e16bc1" -} diff --git a/Cargo.lock b/Cargo.lock index 6ad36146..421ffecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -221,7 +221,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -1459,6 +1459,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1728,6 +1739,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cranelift-bforest" version = "0.113.1" @@ -2692,6 +2712,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.1", + "web-time", +] + [[package]] name = "ff" version = "0.12.1" @@ -3190,6 +3221,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -5938,7 +5970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6264,6 +6296,7 @@ dependencies = [ "cron", "dashmap", "dotenvy", + "ferroid", "futures", "getrandom 0.2.17", "hex", @@ -6299,6 +6332,7 @@ dependencies = [ "sha1", "sha2", "slug", + "sqlparser", "sqlx", "tantivy", "tauri", @@ -6370,6 +6404,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -6427,6 +6472,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_hc" version = "0.2.0" @@ -6489,6 +6540,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recursive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0786a43debb760f491b1bc0269fe5e84155353c67482b9e60d0cfb596054b43e" +dependencies = [ + "recursive-proc-macro-impl", + "stacker", +] + +[[package]] +name = "recursive-proc-macro-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -7511,7 +7582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -7522,7 +7593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -7797,6 +7868,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" +[[package]] +name = "sqlparser" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c6d1b651dc4edf07eead2a0c6c78016ce971bc2c10da5266861b13f25e7cec" +dependencies = [ + "log", + "recursive", +] + [[package]] name = "sqlx" version = "0.8.6" @@ -7995,6 +8076,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index f1106108..1286f92e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,6 +164,7 @@ aws-sdk-s3 = { version = "1", optional = true } aws-config = { version = "1", optional = true } regex = "1.12.3" tera = "1" +sqlparser = "0.62" lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "builder", "tokio1-rustls-tls", "hostname"] } tauri = { version = "2", features = ["devtools"], optional = true } @@ -187,6 +188,7 @@ utoipa-swagger-ui = { version = "9", features = ["axum"], optional = true } ts-rs = { version = "12", optional = true, features = ["chrono-impl"] } inventory = "0.3.24" raisfast-derive = { path = "raisfast-derive" } +ferroid = { version = "2.0.0", features = ["snowflake", "atomic", "basic", "std", "alloc"] } [dev-dependencies] http-body-util = "0.1.3" diff --git a/frontend/sdk/src/generated/types.ts b/frontend/sdk/src/generated/types.ts index 46563bb3..2b31bdaa 100644 --- a/frontend/sdk/src/generated/types.ts +++ b/frontend/sdk/src/generated/types.ts @@ -8,14 +8,11 @@ export type AdminCommentListQuery = { }; export type AdminCommentRow = { - id: bigint; - post_id: bigint; + id: string; post_title: string; - created_by: bigint | null; nickname: string | null; email: string | null; content: string; - parent_id: bigint | null; status: CommentStatus; created_at: string; }; @@ -26,12 +23,6 @@ export type AdminPageListQuery = { status: PageStatus | null; }; -export type AdminPostListQuery = { - page: bigint | null; - page_size: bigint | null; - status: PostStatus | null; -}; - export type AdminWalletOperationRequest = { user_id: string; currency: string; @@ -52,54 +43,53 @@ export type ApiAccess = typeof ApiAccess[keyof typeof ApiAccess]; export type ApiConfig = { /** - * 列表查询(GET /cms/{plural}) + * List query (GET /cms/{plural}) */ list: ApiEndpointConfig; /** - * 单条查询(GET /cms/{plural}/{id}) + * Single query (GET /cms/{plural}/{id}) */ get: ApiEndpointConfig; /** - * 创建(POST /cms/{plural}) + * Create (POST /cms/{plural}) */ create: ApiEndpointConfig; /** - * 更新(PUT /cms/{plural}/{id}) + * Update (PUT /cms/{plural}/{id}) */ update: ApiEndpointConfig; /** - * 删除(DELETE /cms/{plural}/{id}) + * Delete (DELETE /cms/{plural}/{id}) */ delete: ApiEndpointConfig; }; export type ApiEndpointConfig = { /** - * 访问级别:none / public / member / admin + * Access level: none / public / member / admin */ access: ApiAccess; /** - * 数据过滤表达式(对所有通过 access 检查的请求生效) + * Data filter expression (applies to all requests passing access check) */ filter: string | null; /** - * 已登录用户的额外过滤(与 filter 取 OR) + * Additional filter for authenticated users (ORed with filter) */ filter_auth: string | null; /** - * 是否启用服务端缓存(默认 false) + * Whether to enable server-side caching (default false) */ cache: boolean; /** - * API 返回字段白名单(默认空=返回全部非 private 字段) - * 仅对 list/get 端点有效,create/update/delete 忽略此配置 + * API return field whitelist (default empty = return all non-private fields) + * Only effective for list/get endpoints; create/update/delete ignore this setting */ fields: Array | null; }; export type ApiTokenListItem = { - id: bigint; - document_id: string; + id: string; name: string; token_prefix: string; scopes: string; @@ -109,10 +99,9 @@ export type ApiTokenListItem = { }; export type AuditEntry = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; - actor_id: bigint | null; + actor_id: string | null; actor_role: string | null; action: string; subject: string; @@ -164,8 +153,7 @@ export type BindPhoneRequest = { }; export type Category = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; name: string; slug: string; @@ -176,10 +164,10 @@ export type Category = { og_title: string | null; og_description: string | null; og_image: string | null; - parent_id: bigint | null; + parent_id: string | null; sort_order: bigint; - created_by: bigint | null; - updated_by: bigint | null; + created_by: string | null; + updated_by: string | null; created_at: string; updated_at: string; }; @@ -196,16 +184,15 @@ export const CommentOpenStatus = { export type CommentOpenStatus = typeof CommentOpenStatus[keyof typeof CommentOpenStatus]; export type CommentResponse = { - id: bigint; - document_id: string; - post_id: bigint; - created_by: bigint | null; + id: string; nickname: string | null; content: string; - parent_id: bigint | null; depth: number; replies: Array; created_at: string; + post_id: string | null; + created_by: string | null; + parent_id: string | null; }; export const CommentStatus = { @@ -222,63 +209,62 @@ export const ContentKind = { export type ContentKind = typeof ContentKind[keyof typeof ContentKind]; export type ContentRevision = { - id: bigint; - document_id: string; + id: string; content_type: string; record_id: string; revision_number: bigint; snapshot: string; - created_by: bigint | null; + created_by: string | null; created_at: string; }; export type ContentTypeSchema = { /** - * 显示名称(如 "Post") + * Display name (e.g., "Post") */ name: string; /** - * 单数标识(如 "post"),用于 API 路径和注册表 key + * Singular identifier (e.g., "post"), used for API paths and registry key */ singular: string; /** - * 复数标识(如 "posts"),用于 API 路径 + * Plural identifier (e.g., "posts"), used for API paths */ plural: string; /** - * 数据库表名 + * Database table name */ table: string; /** - * 类型:collection(多条记录)或 single(仅一条记录) + * Kind: collection (multiple records) or single (only one record) */ kind: ContentKind; /** - * 描述 + * Description */ description: string; /** - * 字段列表 + * Field list */ fields: Array; /** - * 自动从哪个字段生成 slug + * Which field to auto-generate slug from */ slug_field: string | null; /** - * 是否为内置 content type(内置 CT 不注入默认字段,字段全部显式定义) + * Whether this is a built-in content type (built-in CTs don't inject default fields; all fields are explicitly defined) */ builtin: boolean; /** - * 声明实现的 Protocol 列表(如 ["versionable", "soft_deletable"]) + * Declared Protocol list (e.g., ["versionable", "soft_deletable"]) */ implements: Array>; /** - * 索引定义 + * Index definitions */ indexes: Array; /** - * API 访问控制配置 + * API access control configuration */ api: ApiConfig; }; @@ -334,7 +320,6 @@ export type CreateTagRequest = { export type CreateTokenResult = { id: string; - document_id: string; name: string; token: string; token_prefix: string; @@ -351,7 +336,7 @@ export type CreateWorkflowRequest = { }; export type CredentialResponse = { - id: bigint; + id: string; auth_type: AuthType; identifier: string; verified: boolean; @@ -367,9 +352,8 @@ export const CronExecStatus = { export type CronExecStatus = typeof CronExecStatus[keyof typeof CronExecStatus]; export type CronExecutionLog = { - id: bigint; - document_id: string; - schedule_id: bigint; + id: string; + schedule_id: string; job_type: string; label: string; status: CronExecStatus; @@ -380,8 +364,7 @@ export type CronExecutionLog = { }; export type CronSchedule = { - id: bigint; - document_id: string; + id: string; label: string; job_type: string; payload: string | null; @@ -414,67 +397,67 @@ export type FaqItem = { export type FieldSchema = { /** - * 字段名 + * Field name */ name: string; /** - * 字段类型 + * Field type */ field_type: FieldType; /** - * 是否必填 + * Whether required */ required: boolean; /** - * 是否唯一 + * Whether unique */ unique: boolean; /** - * 默认值 + * Default value */ default: unknown; /** - * 私有字段,公开 API 响应中隐藏(仅 admin API 返回) + * Private field, hidden in public API responses (only returned by admin API) */ private: boolean; /** - * 创建后不可修改 + * Immutable after creation */ immutable: boolean; /** - * Admin UI 显示标签 + * Admin UI display label */ label: string | null; /** - * 字段说明 + * Field description */ description: string | null; /** - * 最大长度(text/email/password) + * Maximum length (text/email/password) */ max_length: number | null; /** - * 最小值(数值类型) + * Minimum value (numeric types) */ min: number | null; /** - * 最大值(数值类型) + * Maximum value (numeric types) */ max: number | null; /** - * 正则校验(text/email) + * Regex validation (text/email) */ pattern: string | null; /** - * 关系配置(仅 relation 类型) + * Relation configuration (relation type only) */ relation: RelationConfig | null; /** - * 媒体配置(仅 media 类型) + * Media configuration (media type only) */ media_config: MediaConfig | null; /** - * 枚举值列表(仅 enum 类型) + * Enum values list (enum type only) */ enum_values: Array | null; }; @@ -521,11 +504,11 @@ export type GalleryImage = { export type IndexDef = { /** - * 索引包含的字段 + * Fields included in the index */ fields: Array; /** - * 是否唯一索引 + * Whether this is a unique index */ unique: boolean; }; @@ -560,18 +543,17 @@ export type LoginResponse = { export type MediaConfig = { /** - * 接受的 MIME 类型 + * Accepted MIME types */ accept: Array; /** - * 最大文件数量 + * Maximum file count */ max_count: number; }; export type MediaResponse = { id: string; - user_id: string; filename: string; url: string; mimetype: string; @@ -633,8 +615,7 @@ export const OptionType = { export type OptionType = typeof OptionType[keyof typeof OptionType]; export type Page = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; title: string; slug: string; @@ -644,11 +625,11 @@ export type Page = { meta_description: string | null; og_image: string | null; template: string; - parent_id: bigint | null; + parent_id: string | null; sort_order: bigint; status: PageStatus; - created_by: bigint; - updated_by: bigint | null; + created_by: string; + updated_by: string | null; cover_image: string | null; password: string | null; comment_status: CommentOpenStatus; @@ -794,9 +775,8 @@ export const PageStatus = { export type PageStatus = typeof PageStatus[keyof typeof PageStatus]; export type Permission = { - id: bigint; - document_id: string; - role_id: bigint; + id: string; + role_id: string; action: string; subject: string; fields: string | null; @@ -856,14 +836,6 @@ export type PluginMetrics = { total_duration_us: bigint; }; -export type PostListQuery = { - page: bigint | null; - page_size: bigint | null; - category_id: string | null; - tag_id: string | null; - q: string | null; -}; - export type PostResponse = { id: string; title: string; @@ -872,9 +844,7 @@ export type PostResponse = { excerpt: string | null; cover_image: string | null; status: PostStatus; - created_by: bigint; author_name: string | null; - category_id: bigint | null; category_name: string | null; tags: Array; view_count: bigint; @@ -895,6 +865,8 @@ export type PostResponse = { published_at: string | null; title_highlight: string | null; excerpt_highlight: string | null; + created_by: string | null; + category_id: string | null; }; export const PostStatus = { @@ -938,15 +910,15 @@ export type RegisteredVia = typeof RegisteredVia[keyof typeof RegisteredVia]; export type RelationConfig = { relation_type: RelationType; /** - * 目标 content type 名称 + * Target content type name */ target: string; /** - * 多对多中间表名 + * Many-to-many junction table name */ through: string | null; /** - * 外键列名(默认为 "{target}_id") + * Foreign key column name (defaults to "{target}_id") */ foreign_key: string | null; }; @@ -980,15 +952,14 @@ export type ResetPasswordRequest = { }; export type ReusableBlock = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; name: string; block_type: string; content: string; description: string | null; - created_by: bigint | null; - updated_by: bigint | null; + created_by: string | null; + updated_by: string | null; created_at: string; updated_at: string; }; @@ -998,15 +969,14 @@ export type ReversalRequest = { }; export type RevisionSummary = { - id: bigint; + id: string; revision_number: bigint; - created_by: bigint | null; + created_by: string | null; created_at: string; }; export type Role = { - id: bigint; - document_id: string; + id: string; name: string; description: string | null; is_system: boolean; @@ -1057,9 +1027,8 @@ export type StepDef = { }; export type StepLog = { - id: bigint; - document_id: string; - instance_id: bigint; + id: string; + instance_id: string; step_id: string; step_name: string; status: WorkflowStepStatus; @@ -1080,8 +1049,7 @@ export const StepType = { export type StepType = typeof StepType[keyof typeof StepType]; export type Tag = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; name: string; slug: string; @@ -1092,14 +1060,14 @@ export type Tag = { og_title: string | null; og_description: string | null; og_image: string | null; - created_by: bigint | null; - updated_by: bigint | null; + created_by: string | null; + updated_by: string | null; created_at: string; updated_at: string; }; export type TagBrief = { - id: bigint; + id: string; name: string; slug: string; }; @@ -1113,8 +1081,7 @@ export type TeamMember = { }; export type Tenant = { - id: bigint; - document_id: string; + id: string; name: string; domain: string | null; config: string; @@ -1268,7 +1235,9 @@ export const WalletReferenceType = { api_usage: "api_usage", points_mall: "points_mall", order: "order", - expiry: "expiry" + expiry: "expiry", + payment: "payment", + payment_refund: "payment_refund" } as const; export type WalletReferenceType = typeof WalletReferenceType[keyof typeof WalletReferenceType]; @@ -1295,10 +1264,10 @@ export type WalletTransactionResponse = { tx_type: WalletTxType; currency: string; transaction_no: string; - related_tx_id: string | null; reference_type: WalletReferenceType | null; reference_id: string | null; metadata: string | null; + related_tx_id: string | null; created_at: string; }; @@ -1311,195 +1280,8 @@ export const WalletTxType = { } as const; export type WalletTxType = typeof WalletTxType[keyof typeof WalletTxType]; -// ─── Product types ─── - -export type ProductResponse = { - id: string; - category_id: string | null; - title: string; - description: string | null; - cover_url: string | null; - product_type: string; - fulfillment_type: string; - delivery_hook: string | null; - weight: number | null; - price: number; - currency: string; - status: string; - attributes: string | null; - sort_order: number; - slug: string | null; - content: string | null; - image_ids: unknown; - original_price: number | null; - specs: unknown; - unit: string; - min_purchase: number; - max_purchase: number | null; - total_sales: number; - virtual_sales: number; - meta_title: string | null; - meta_description: string | null; - published_at: string | null; - version: number; - created_at: string; - updated_at: string; -}; - -// ─── Order types ─── - -export type OrderItemResponse = { - id: string; - order_id: string; - product_id: string | null; - title: string; - description: string | null; - unit_price: number; - quantity: number; - subtotal: number; - cover_url: string | null; - attributes: string | null; - created_at: string; -}; - -export type OrderResponse = { - id: string; - user_id: string; - order_no: string; - subtotal: number; - discount_amount: number; - shipping_amount: number; - total_amount: number; - currency: string; - status: string; - buyer_name: string | null; - buyer_phone: string | null; - buyer_email: string | null; - shipping_address: string | null; - tracking_no: string | null; - carrier: string | null; - remark: string | null; - admin_remark: string | null; - delivery_data: string | null; - paid_at: string | null; - completed_at: string | null; - cancelled_at: string | null; - created_at: string; - updated_at: string; - items: OrderItemResponse[]; -}; - -export type OrderStatsResponse = { - total_orders: number; - pending_orders: number; - paid_orders: number; - completed_orders: number; - total_revenue: number; -}; - -// ─── Payment types ─── - -export const PaymentStatus = { - pending: "pending", - processing: "processing", - paid: "paid", - failed: "failed", - cancelled: "cancelled", - expired: "expired", - refunded: "refunded", - partially_refunded: "partially_refunded", -} as const; -export type PaymentStatus = typeof PaymentStatus[keyof typeof PaymentStatus]; - -export type PaymentOrderResponse = { - id: string; - user_id: number; - order_id: string | null; - title: string; - amount: number; - currency: string; - channel_id: string; - provider: string; - provider_order_id: string | null; - provider_method: string | null; - status: PaymentStatus; - return_url: string | null; - version: number; - provider_data: string | null; - client_ip: string | null; - client_language: string | null; - client_country: string | null; - client_user_agent: string | null; - channel_selected_by: string | null; - metadata: string | null; - redirect_url: string | null; - qr_code: string | null; - client_secret: string | null; - paid_at: string | null; - cancelled_at: string | null; - expired_at: string | null; - created_at: string; - updated_at: string; -}; - -export type PaymentChannelResponse = { - id: string; - provider: string; - name: string; - is_live: boolean; - credentials_masked: string; - webhook_secret_set: boolean; - settings: string | null; - is_active: boolean; - sort_order: number; - version: number; - created_at: string; - updated_at: string; -}; - -export type PaymentTransactionResponse = { - id: string; - payment_order_id: number; - order_id: string | null; - user_id: number; - tx_type: string; - amount: number; - currency: string; - provider_tx_id: string; - status: string; - created_at: string; -}; - -export type PaymentRefundResponse = { - id: string; - payment_order_id: number; - order_id: string | null; - user_id: number; - amount: number; - currency: string; - reason: string | null; - provider_refund_id: string | null; - status: string; - created_at: string; - updated_at: string; -}; - -export type AvailableChannelItem = { - channel_id: string; - provider: string; - name: string; - is_recommended: boolean; - sort_order: number; -}; - -export type AvailableChannelsResponse = { - recommended_channel_id: string | null; - channels: AvailableChannelItem[]; -}; - export type WebhookSubscription = { - id: bigint; - document_id: string; + id: string; tenant_id: string | null; url: string; secret: string; @@ -1511,8 +1293,7 @@ export type WebhookSubscription = { }; export type WorkflowDefinition = { - id: bigint; - document_id: string; + id: string; name: string; description: string | null; steps: string; @@ -1524,13 +1305,12 @@ export type WorkflowDefinition = { }; export type WorkflowInstance = { - id: bigint; - document_id: string; - definition_id: bigint; + id: string; + definition_id: string; status: WorkflowInstanceStatus; current_step: string | null; context: string; - triggered_by: bigint | null; + triggered_by: string | null; started_at: string; completed_at: string | null; updated_at: string; diff --git a/justfile b/justfile index 323107aa..7e4ba27e 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ set dotenv-load db := "sqlite" -db_url := "sqlite:./data/raisfast.db" +db_url := "sqlite:./storage/db/raisfast.db?mode=rwc" plugin_type := "all" # ── 默认 ────────────────────────────────────────────────────────── diff --git a/migrations/mysql/schema.mysql.sql b/migrations/mysql/schema.mysql.sql index de35f063..3211028c 100644 --- a/migrations/mysql/schema.mysql.sql +++ b/migrations/mysql/schema.mysql.sql @@ -13,7 +13,6 @@ -- 租户表 CREATE TABLE IF NOT EXISTS tenants ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, domain VARCHAR(255) UNIQUE, config JSON NOT NULL, @@ -23,13 +22,12 @@ CREATE TABLE IF NOT EXISTS tenants ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 默认租户 -INSERT IGNORE INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES - ('default', 'Default', NULL, '{}', 'active', NOW(), NOW()); +INSERT IGNORE INTO tenants (name, domain, config, status, created_at, updated_at) VALUES + ('Default', NULL, '{}', 'active', NOW(), NOW()); -- 用户 CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', username VARCHAR(255) UNIQUE NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'reader', @@ -54,7 +52,6 @@ CREATE INDEX idx_users_tenant ON users(tenant_id); -- 用户凭据 CREATE TABLE IF NOT EXISTS user_credentials ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, auth_type VARCHAR(100) NOT NULL, identifier VARCHAR(500) NOT NULL, @@ -73,7 +70,6 @@ CREATE INDEX idx_user_credentials_type ON user_credentials(auth_type); -- OAuth 账号绑定 CREATE TABLE IF NOT EXISTS oauth_accounts ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, provider VARCHAR(50) NOT NULL, provider_user_id VARCHAR(255) NOT NULL, @@ -96,7 +92,6 @@ CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider, provider_us -- OAuth 短期 state 存储(PKCE) CREATE TABLE IF NOT EXISTS oauth_states ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, provider VARCHAR(50) NOT NULL, code_verifier VARCHAR(255) NOT NULL, user_id BIGINT, @@ -109,7 +104,6 @@ CREATE INDEX idx_oauth_states_expires ON oauth_states(expires_at); -- 币种配置 CREATE TABLE IF NOT EXISTS currencies ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, code VARCHAR(10) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, decimals INT NOT NULL DEFAULT 0, @@ -125,7 +119,6 @@ CREATE UNIQUE INDEX idx_currencies_code ON currencies(code); CREATE TABLE IF NOT EXISTS wallets ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, currency VARCHAR(50) NOT NULL, @@ -144,7 +137,6 @@ CREATE INDEX idx_wallets_tenant ON wallets(tenant_id); CREATE TABLE IF NOT EXISTS wallet_transactions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', wallet_id BIGINT NOT NULL, user_id BIGINT NOT NULL, @@ -175,7 +167,6 @@ CREATE INDEX idx_wallet_transactions_tenant ON wallet_transactions(tenant_id); -- Refresh Tokens CREATE TABLE IF NOT EXISTS refresh_tokens ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, token VARCHAR(500) UNIQUE NOT NULL, expires_at DATETIME(3) NOT NULL, @@ -190,7 +181,6 @@ CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at); -- 站点配置 CREATE TABLE IF NOT EXISTS options ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', `option_key` VARCHAR(255) NOT NULL, value TEXT NOT NULL, @@ -211,7 +201,6 @@ CREATE INDEX idx_options_tenant_option_key ON options(tenant_id, `option_key`); -- RBAC 角色 CREATE TABLE IF NOT EXISTS roles ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', name VARCHAR(100) NOT NULL UNIQUE, description TEXT, @@ -225,7 +214,6 @@ CREATE INDEX idx_roles_tenant ON roles(tenant_id); -- RBAC 权限 CREATE TABLE IF NOT EXISTS permissions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', role_id BIGINT NOT NULL, action VARCHAR(255) NOT NULL, @@ -243,7 +231,6 @@ CREATE INDEX idx_permissions_tenant ON permissions(tenant_id); -- 审计日志 CREATE TABLE IF NOT EXISTS audit_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', actor_id BIGINT, actor_role VARCHAR(50), @@ -264,7 +251,6 @@ CREATE INDEX idx_audit_log_tenant ON audit_log(tenant_id); -- API Token CREATE TABLE IF NOT EXISTS api_tokens ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, name VARCHAR(255) NOT NULL, token_hash VARCHAR(255) UNIQUE NOT NULL, @@ -282,7 +268,6 @@ CREATE INDEX idx_api_tokens_token_hash ON api_tokens(token_hash); -- Webhook 订阅 CREATE TABLE IF NOT EXISTS webhook_subscriptions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', url VARCHAR(1024) NOT NULL, secret VARCHAR(255) NOT NULL, @@ -311,9 +296,8 @@ CREATE INDEX idx_plugin_storage_plugin ON plugin_storage(plugin_id); -- 内容版本历史 CREATE TABLE IF NOT EXISTS content_revisions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, content_type VARCHAR(100) NOT NULL, - record_id TEXT NOT NULL, + record_id BIGINT NOT NULL, revision_number INT NOT NULL, snapshot TEXT NOT NULL, created_by BIGINT, @@ -329,7 +313,6 @@ CREATE INDEX idx_revisions_ct_record_rev -- 密码重置令牌 CREATE TABLE IF NOT EXISTS password_reset_tokens ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, token VARCHAR(255) NOT NULL UNIQUE, expires_at DATETIME(3) NOT NULL, @@ -345,7 +328,6 @@ CREATE INDEX idx_password_reset_tokens_expires_at ON password_reset_tokens(expir -- 短信验证码 CREATE TABLE IF NOT EXISTS sms_codes ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, phone VARCHAR(50) NOT NULL, code VARCHAR(20) NOT NULL, purpose VARCHAR(50) NOT NULL, @@ -362,7 +344,6 @@ CREATE INDEX idx_sms_codes_expires ON sms_codes(expires_at); -- 邮箱验证令牌 CREATE TABLE IF NOT EXISTS email_verification_tokens ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL, token VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL, @@ -379,7 +360,6 @@ CREATE INDEX idx_email_verification_tokens_expires ON email_verification_tokens( -- 后台任务队列 CREATE TABLE IF NOT EXISTS jobs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, job_type VARCHAR(100) NOT NULL, payload TEXT NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', @@ -398,7 +378,6 @@ CREATE INDEX idx_jobs_type ON jobs(job_type); -- 定时任务调度 CREATE TABLE IF NOT EXISTS cron_schedules ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, label VARCHAR(255) NOT NULL, job_type VARCHAR(100) NOT NULL, payload TEXT, @@ -418,7 +397,6 @@ CREATE INDEX idx_cron_plugin ON cron_schedules(plugin_id); -- Cron 执行历史 CREATE TABLE IF NOT EXISTS cron_execution_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, schedule_id BIGINT NOT NULL, job_type VARCHAR(100) NOT NULL, label VARCHAR(255) NOT NULL, @@ -438,7 +416,6 @@ CREATE INDEX idx_cron_log_started ON cron_execution_log(started_at); -- 分类 CREATE TABLE IF NOT EXISTS categories ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', name VARCHAR(255) UNIQUE NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -463,7 +440,6 @@ CREATE INDEX idx_categories_tenant ON categories(tenant_id); -- 标签 CREATE TABLE IF NOT EXISTS tags ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', name VARCHAR(255) UNIQUE NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -485,7 +461,6 @@ CREATE INDEX idx_tags_tenant ON tags(tenant_id); -- 文章 CREATE TABLE IF NOT EXISTS posts ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', title VARCHAR(500) NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -543,7 +518,6 @@ CREATE INDEX idx_posts_tags_tag_id ON posts_tags(tag_id); -- 评论 CREATE TABLE IF NOT EXISTS comments ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', post_id BIGINT NOT NULL, created_by BIGINT, @@ -574,7 +548,6 @@ CREATE INDEX idx_comments_tenant ON comments(tenant_id); CREATE TABLE IF NOT EXISTS pages ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', title VARCHAR(500) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, @@ -611,7 +584,6 @@ CREATE INDEX idx_pages_tenant_status ON pages(tenant_id, status); CREATE TABLE IF NOT EXISTS reusable_blocks ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', name VARCHAR(255) NOT NULL, block_type VARCHAR(100) NOT NULL, @@ -629,7 +601,6 @@ CREATE INDEX idx_reusable_blocks_tenant ON reusable_blocks(tenant_id); CREATE TABLE IF NOT EXISTS media ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, filename VARCHAR(255) NOT NULL, @@ -655,7 +626,6 @@ CREATE INDEX idx_media_tenant ON media(tenant_id); CREATE TABLE IF NOT EXISTS workflow_definitions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, steps JSON NOT NULL, @@ -668,7 +638,6 @@ CREATE TABLE IF NOT EXISTS workflow_definitions ( CREATE TABLE IF NOT EXISTS workflow_instances ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, definition_id BIGINT NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'running', current_step VARCHAR(100), @@ -685,7 +654,6 @@ CREATE INDEX idx_wf_instances_status ON workflow_instances(status); CREATE TABLE IF NOT EXISTS workflow_step_logs ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, instance_id BIGINT NOT NULL, step_id VARCHAR(100) NOT NULL, step_name VARCHAR(255) NOT NULL, @@ -703,7 +671,6 @@ CREATE INDEX idx_wf_step_logs_instance ON workflow_step_logs(instance_id); -- Products CREATE TABLE IF NOT EXISTS products ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', category_id BIGINT, title VARCHAR(500) NOT NULL, @@ -750,7 +717,6 @@ CREATE INDEX idx_products_tenant ON products(tenant_id); -- Product Variants CREATE TABLE IF NOT EXISTS product_variants ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', product_id BIGINT NOT NULL, sku VARCHAR(100) UNIQUE, @@ -773,7 +739,6 @@ CREATE INDEX idx_product_variants_tenant ON product_variants(tenant_id); -- User Addresses CREATE TABLE IF NOT EXISTS user_addresses ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, label VARCHAR(100) NOT NULL DEFAULT '', @@ -799,7 +764,6 @@ CREATE INDEX idx_user_addresses_tenant ON user_addresses(tenant_id); -- Orders CREATE TABLE IF NOT EXISTS orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, order_no VARCHAR(255) NOT NULL UNIQUE, @@ -841,7 +805,6 @@ CREATE INDEX idx_orders_tenant ON orders(tenant_id); -- Order Items CREATE TABLE IF NOT EXISTS order_items ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', order_id BIGINT NOT NULL, product_id BIGINT, @@ -866,7 +829,6 @@ CREATE INDEX idx_order_items_tenant ON order_items(tenant_id); CREATE TABLE IF NOT EXISTS cart_items ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, @@ -885,7 +847,6 @@ CREATE INDEX idx_cart_items_tenant ON cart_items(tenant_id); CREATE TABLE IF NOT EXISTS payment_channels ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', provider VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, @@ -908,7 +869,6 @@ CREATE INDEX idx_payment_channels_tenant ON payment_channels(tenant_id); -- Payment Orders CREATE TABLE IF NOT EXISTS payment_orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, order_id VARCHAR(36), @@ -950,7 +910,6 @@ CREATE INDEX idx_payment_orders_tenant ON payment_orders(tenant_id); -- Payment Transactions CREATE TABLE IF NOT EXISTS payment_transactions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', payment_order_id BIGINT NOT NULL, order_id VARCHAR(36), @@ -973,7 +932,6 @@ CREATE INDEX idx_payment_transactions_tenant ON payment_transactions(tenant_id); -- Payment Refunds CREATE TABLE IF NOT EXISTS payment_refunds ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', payment_order_id BIGINT NOT NULL, order_id VARCHAR(36), @@ -999,7 +957,6 @@ CREATE INDEX idx_payment_refunds_tenant ON payment_refunds(tenant_id); -- Wallet Outbox (ensures wallet operations are never lost) CREATE TABLE IF NOT EXISTS wallet_outbox ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id VARCHAR(36) NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, currency VARCHAR(10) NOT NULL, @@ -1026,45 +983,45 @@ CREATE TABLE IF NOT EXISTS wallet_outbox ( -- ============================================================ -- 系统角色 -INSERT IGNORE INTO roles (document_id, tenant_id, name, description, is_system, created_at, updated_at) VALUES - ('role-admin', 'default', 'admin', '超级管理员', TRUE, NOW(), NOW()), - ('role-editor', 'default', 'editor', '编辑', FALSE, NOW(), NOW()), - ('role-author', 'default', 'author', '作者', FALSE, NOW(), NOW()), - ('role-reader', 'default', 'reader', '读者', TRUE, NOW(), NOW()); +INSERT IGNORE INTO roles (tenant_id, name, description, is_system, created_at, updated_at) VALUES + ('default', 'admin', '超级管理员', TRUE, NOW(), NOW()), + ('default', 'editor', '编辑', FALSE, NOW(), NOW()), + ('default', 'author', '作者', FALSE, NOW(), NOW()), + ('default', 'reader', '读者', TRUE, NOW(), NOW()); -- admin 全局权限 -INSERT IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-admin-all', 'default', (SELECT id FROM roles WHERE document_id = 'role-admin'), '*', '*', '["*"]', NULL, NOW()); +INSERT IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'admin'), '*', '*', '["*"]', NULL, NOW()); -- editor 权限 -INSERT IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-editor-ct-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW()); +INSERT IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW()); -- author 权限 -INSERT IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-author-post-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()), - ('perm-author-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()), - ('perm-author-post-update', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()), - ('perm-author-post-delete', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()); +INSERT IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()); -- reader 权限 -INSERT IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-reader-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()), - ('perm-reader-comment-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW()); +INSERT IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW()); -- 站点配置 -INSERT IGNORE INTO options (document_id, tenant_id, `option_key`, value, `type`, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES - ('opt-site-title', 'default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', TRUE, TRUE, 1, NOW()), - ('opt-site-desc', 'default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', TRUE, TRUE, 2, NOW()), - ('opt-site-url', 'default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, TRUE, TRUE, 3, NOW()), - ('opt-admin-email', 'default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, FALSE, TRUE, 4, NOW()), - ('opt-timezone', 'default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()), - ('opt-date-fmt', 'default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()), - ('opt-per-page', 'default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()), - ('opt-rss-items', 'default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()), - ('opt-permalink', 'default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()), - ('opt-comment-mod', 'default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, FALSE, TRUE, 20, NOW()), - ('opt-comment-order', 'default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()), - ('opt-default-role', 'default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()), - ('opt-theme', 'default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()), - ('opt-maintenance', 'default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, TRUE, TRUE, 31, NOW()); +INSERT IGNORE INTO options (tenant_id, `option_key`, value, `type`, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES + ('default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', TRUE, TRUE, 1, NOW()), + ('default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', TRUE, TRUE, 2, NOW()), + ('default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, TRUE, TRUE, 3, NOW()), + ('default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, FALSE, TRUE, 4, NOW()), + ('default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()), + ('default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()), + ('default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()), + ('default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()), + ('default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()), + ('default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, FALSE, TRUE, 20, NOW()), + ('default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()), + ('default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()), + ('default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()), + ('default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, TRUE, TRUE, 31, NOW()); diff --git a/migrations/postgres/schema.postgres.sql b/migrations/postgres/schema.postgres.sql index ea5ef1cc..f48813df 100644 --- a/migrations/postgres/schema.postgres.sql +++ b/migrations/postgres/schema.postgres.sql @@ -9,7 +9,6 @@ -- 租户表 CREATE TABLE IF NOT EXISTS tenants ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, domain VARCHAR(255) UNIQUE, config JSONB NOT NULL DEFAULT '{}', @@ -19,14 +18,13 @@ CREATE TABLE IF NOT EXISTS tenants ( ); -- 默认租户 -INSERT INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES - ('default', 'Default', NULL, '{}', 'active', NOW(), NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO tenants (name, domain, config, status, created_at, updated_at) VALUES + ('Default', NULL, '{}', 'active', NOW(), NOW()) +ON CONFLICT (name) DO NOTHING; -- 用户 CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', username VARCHAR(255) UNIQUE NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'reader', @@ -51,7 +49,6 @@ CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id); -- 用户凭据 CREATE TABLE IF NOT EXISTS user_credentials ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), auth_type VARCHAR(100) NOT NULL, identifier VARCHAR(500) NOT NULL, @@ -69,7 +66,6 @@ CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(auth_ty -- OAuth 账号绑定 CREATE TABLE IF NOT EXISTS oauth_accounts ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), provider VARCHAR(50) NOT NULL, provider_user_id VARCHAR(255) NOT NULL, @@ -91,7 +87,6 @@ CREATE INDEX IF NOT EXISTS idx_oauth_accounts_provider ON oauth_accounts(provide -- OAuth 短期 state 存储(PKCE) CREATE TABLE IF NOT EXISTS oauth_states ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, provider VARCHAR(50) NOT NULL, code_verifier VARCHAR(255) NOT NULL, user_id BIGINT, @@ -104,7 +99,6 @@ CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at); -- 币种配置 CREATE TABLE IF NOT EXISTS currencies ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, code VARCHAR(10) NOT NULL UNIQUE CHECK(code = UPPER(code) AND LENGTH(code) BETWEEN 1 AND 10), name TEXT NOT NULL, decimals INTEGER NOT NULL DEFAULT 0 CHECK(decimals BETWEEN 0 AND 18), @@ -118,7 +112,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_code ON currencies(code); CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), currency VARCHAR(50) NOT NULL, @@ -136,7 +129,6 @@ CREATE INDEX IF NOT EXISTS idx_wallets_tenant ON wallets(tenant_id); CREATE TABLE IF NOT EXISTS wallet_transactions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', wallet_id BIGINT NOT NULL REFERENCES wallets(id), user_id BIGINT NOT NULL REFERENCES users(id), @@ -165,7 +157,6 @@ CREATE INDEX IF NOT EXISTS idx_wallet_transactions_tenant ON wallet_transactions -- Refresh Tokens CREATE TABLE IF NOT EXISTS refresh_tokens ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), token VARCHAR(500) UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL, @@ -179,7 +170,6 @@ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expir -- 站点配置 CREATE TABLE IF NOT EXISTS options ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', option_key VARCHAR(255) NOT NULL, value TEXT NOT NULL, @@ -200,7 +190,6 @@ CREATE INDEX IF NOT EXISTS idx_options_tenant_option_key ON options(tenant_id, o -- RBAC 角色 CREATE TABLE IF NOT EXISTS roles ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name VARCHAR(255) NOT NULL UNIQUE, description TEXT, @@ -214,7 +203,6 @@ CREATE INDEX IF NOT EXISTS idx_roles_tenant ON roles(tenant_id); -- RBAC 权限 CREATE TABLE IF NOT EXISTS permissions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', role_id BIGINT NOT NULL REFERENCES roles(id), action VARCHAR(255) NOT NULL, @@ -231,7 +219,6 @@ CREATE INDEX IF NOT EXISTS idx_permissions_tenant ON permissions(tenant_id); -- 审计日志 CREATE TABLE IF NOT EXISTS audit_log ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', actor_id BIGINT, actor_role VARCHAR(50), @@ -252,7 +239,6 @@ CREATE INDEX IF NOT EXISTS idx_audit_log_tenant ON audit_log(tenant_id); -- API Token CREATE TABLE IF NOT EXISTS api_tokens ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), name VARCHAR(255) NOT NULL, token_hash VARCHAR(255) UNIQUE NOT NULL, @@ -269,7 +255,6 @@ CREATE INDEX IF NOT EXISTS idx_api_tokens_token_hash ON api_tokens(token_hash); -- Webhook 订阅 CREATE TABLE IF NOT EXISTS webhook_subscriptions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', url VARCHAR(1024) NOT NULL, secret VARCHAR(255) NOT NULL, @@ -298,9 +283,8 @@ CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin ON plugin_storage(plugin_id -- 内容版本历史 CREATE TABLE IF NOT EXISTS content_revisions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, content_type VARCHAR(100) NOT NULL, - record_id VARCHAR(36) NOT NULL, + record_id BIGINT NOT NULL, revision_number INTEGER NOT NULL, snapshot JSONB NOT NULL, created_by BIGINT, @@ -316,7 +300,6 @@ CREATE INDEX IF NOT EXISTS idx_revisions_ct_record_rev -- 密码重置令牌 CREATE TABLE IF NOT EXISTS password_reset_tokens ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, @@ -331,7 +314,6 @@ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at ON password_rese -- 短信验证码 CREATE TABLE IF NOT EXISTS sms_codes ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, phone VARCHAR(50) NOT NULL, code VARCHAR(20) NOT NULL, purpose VARCHAR(50) NOT NULL, @@ -348,7 +330,6 @@ CREATE INDEX IF NOT EXISTS idx_sms_codes_expires ON sms_codes(expires_at); -- 邮箱验证令牌 CREATE TABLE IF NOT EXISTS email_verification_tokens ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, user_id BIGINT NOT NULL REFERENCES users(id), token VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL, @@ -364,7 +345,6 @@ CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_expires ON email_verifi -- 后台任务队列 CREATE TABLE IF NOT EXISTS jobs ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, job_type VARCHAR(100) NOT NULL, payload TEXT NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', @@ -383,7 +363,6 @@ CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(job_type); -- 定时任务调度 CREATE TABLE IF NOT EXISTS cron_schedules ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, label VARCHAR(255) NOT NULL, job_type VARCHAR(100) NOT NULL, payload TEXT, @@ -403,7 +382,6 @@ CREATE INDEX IF NOT EXISTS idx_cron_plugin ON cron_schedules(plugin_id); -- Cron 执行历史 CREATE TABLE IF NOT EXISTS cron_execution_log ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, schedule_id BIGINT NOT NULL, job_type VARCHAR(100) NOT NULL, label VARCHAR(255) NOT NULL, @@ -423,7 +401,6 @@ CREATE INDEX IF NOT EXISTS idx_cron_log_started ON cron_execution_log(started_at -- 分类 CREATE TABLE IF NOT EXISTS categories ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name VARCHAR(255) UNIQUE NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -447,7 +424,6 @@ CREATE INDEX IF NOT EXISTS idx_categories_tenant ON categories(tenant_id); -- 标签 CREATE TABLE IF NOT EXISTS tags ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name VARCHAR(255) UNIQUE NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -469,7 +445,6 @@ CREATE INDEX IF NOT EXISTS idx_tags_tenant ON tags(tenant_id); -- 文章 CREATE TABLE IF NOT EXISTS posts ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', title TEXT NOT NULL, slug VARCHAR(255) UNIQUE NOT NULL, @@ -523,7 +498,6 @@ CREATE INDEX IF NOT EXISTS idx_posts_tags_tag_id ON posts_tags(tag_id); -- 评论 CREATE TABLE IF NOT EXISTS comments ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', post_id BIGINT NOT NULL REFERENCES posts(id), created_by BIGINT REFERENCES users(id), @@ -551,7 +525,6 @@ CREATE INDEX IF NOT EXISTS idx_comments_tenant ON comments(tenant_id); CREATE TABLE IF NOT EXISTS pages ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', title TEXT NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, @@ -586,7 +559,6 @@ CREATE INDEX IF NOT EXISTS idx_pages_tenant_status ON pages(tenant_id, status); CREATE TABLE IF NOT EXISTS reusable_blocks ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name VARCHAR(255) NOT NULL, block_type TEXT NOT NULL, @@ -604,7 +576,6 @@ CREATE INDEX IF NOT EXISTS idx_reusable_blocks_tenant ON reusable_blocks(tenant_ CREATE TABLE IF NOT EXISTS media ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), filename VARCHAR(255) NOT NULL, @@ -629,7 +600,6 @@ CREATE INDEX IF NOT EXISTS idx_media_tenant ON media(tenant_id); CREATE TABLE IF NOT EXISTS workflow_definitions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, steps JSONB NOT NULL, @@ -642,7 +612,6 @@ CREATE TABLE IF NOT EXISTS workflow_definitions ( CREATE TABLE IF NOT EXISTS workflow_instances ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, definition_id BIGINT NOT NULL REFERENCES workflow_definitions(id), status VARCHAR(50) NOT NULL DEFAULT 'running', current_step VARCHAR(100), @@ -658,7 +627,6 @@ CREATE INDEX IF NOT EXISTS idx_wf_instances_status ON workflow_instances(status) CREATE TABLE IF NOT EXISTS workflow_step_logs ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, instance_id BIGINT NOT NULL REFERENCES workflow_instances(id), step_id VARCHAR(100) NOT NULL, step_name VARCHAR(255) NOT NULL, @@ -675,7 +643,6 @@ CREATE INDEX IF NOT EXISTS idx_wf_step_logs_instance ON workflow_step_logs(insta -- Products CREATE TABLE IF NOT EXISTS products ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', category_id BIGINT REFERENCES categories(id), title TEXT NOT NULL, @@ -721,7 +688,6 @@ CREATE INDEX IF NOT EXISTS idx_products_tenant ON products(tenant_id); -- Product Variants CREATE TABLE IF NOT EXISTS product_variants ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', product_id BIGINT NOT NULL REFERENCES products(id), sku VARCHAR(100) UNIQUE, @@ -743,7 +709,6 @@ CREATE INDEX IF NOT EXISTS idx_product_variants_tenant ON product_variants(tenan -- User Addresses CREATE TABLE IF NOT EXISTS user_addresses ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), label VARCHAR(100) NOT NULL DEFAULT '', @@ -768,7 +733,6 @@ CREATE INDEX IF NOT EXISTS idx_user_addresses_tenant ON user_addresses(tenant_id -- Orders CREATE TABLE IF NOT EXISTS orders ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), order_no VARCHAR(255) NOT NULL UNIQUE, @@ -809,7 +773,6 @@ CREATE INDEX IF NOT EXISTS idx_orders_tenant ON orders(tenant_id); -- Order Items CREATE TABLE IF NOT EXISTS order_items ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', order_id BIGINT NOT NULL REFERENCES orders(id), product_id BIGINT REFERENCES products(id), @@ -831,7 +794,6 @@ CREATE INDEX IF NOT EXISTS idx_order_items_tenant ON order_items(tenant_id); CREATE TABLE IF NOT EXISTS cart_items ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), product_id BIGINT NOT NULL REFERENCES products(id), @@ -846,7 +808,6 @@ CREATE INDEX IF NOT EXISTS idx_cart_items_tenant ON cart_items(tenant_id); CREATE TABLE IF NOT EXISTS payment_channels ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', provider VARCHAR(50) NOT NULL, name VARCHAR(200) NOT NULL, @@ -869,7 +830,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_channels_tenant ON payment_channels(tenan -- Payment Orders CREATE TABLE IF NOT EXISTS payment_orders ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL REFERENCES users(id), order_id VARCHAR(36), @@ -909,7 +869,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_orders_tenant ON payment_orders(tenant_id -- Payment Transactions CREATE TABLE IF NOT EXISTS payment_transactions ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id), order_id VARCHAR(36), @@ -930,7 +889,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_transactions_tenant ON payment_transactio -- Payment Refunds CREATE TABLE IF NOT EXISTS payment_refunds ( id BIGSERIAL PRIMARY KEY, - document_id VARCHAR(36) NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', payment_order_id BIGINT NOT NULL REFERENCES payment_orders(id), order_id VARCHAR(36), @@ -953,7 +911,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_refunds_tenant ON payment_refunds(tenant_ -- Wallet Outbox (ensures wallet operations are never lost) CREATE TABLE IF NOT EXISTS wallet_outbox ( id BIGSERIAL PRIMARY KEY, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id BIGINT NOT NULL, currency TEXT NOT NULL, @@ -981,51 +938,51 @@ CREATE INDEX IF NOT EXISTS idx_wallet_outbox_tenant ON wallet_outbox(tenant_id); -- ============================================================ -- 系统角色 -INSERT INTO roles (document_id, tenant_id, name, description, is_system, created_at, updated_at) VALUES - ('role-admin', 'default', 'admin', '超级管理员', TRUE, NOW(), NOW()), - ('role-editor', 'default', 'editor', '编辑', FALSE, NOW(), NOW()), - ('role-author', 'default', 'author', '作者', FALSE, NOW(), NOW()), - ('role-reader', 'default', 'reader', '读者', TRUE, NOW(), NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO roles (tenant_id, name, description, is_system, created_at, updated_at) VALUES + ('default', 'admin', '超级管理员', TRUE, NOW(), NOW()), + ('default', 'editor', '编辑', FALSE, NOW(), NOW()), + ('default', 'author', '作者', FALSE, NOW(), NOW()), + ('default', 'reader', '读者', TRUE, NOW(), NOW()) +ON CONFLICT (name) DO NOTHING; -- admin 全局权限 -INSERT INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-admin-all', 'default', (SELECT id FROM roles WHERE document_id = 'role-admin'), '*', '*', '["*"]', NULL, NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'admin'), '*', '*', '["*"]', NULL, NOW()) +ON CONFLICT (role_id, action, subject) DO NOTHING; -- editor 权限 -INSERT INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-editor-ct-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, NOW()) +ON CONFLICT (role_id, action, subject) DO NOTHING; -- author 权限 -INSERT INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-author-post-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()), - ('perm-author-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()), - ('perm-author-post-update', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()), - ('perm-author-post-delete', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', NOW()) +ON CONFLICT (role_id, action, subject) DO NOTHING; -- reader 权限 -INSERT INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-reader-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()), - ('perm-reader-comment-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, NOW()), + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, NOW()) +ON CONFLICT (role_id, action, subject) DO NOTHING; -- 站点配置 -INSERT INTO options (document_id, tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES - ('opt-site-title', 'default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', TRUE, TRUE, 1, NOW()), - ('opt-site-desc', 'default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', TRUE, TRUE, 2, NOW()), - ('opt-site-url', 'default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, TRUE, TRUE, 3, NOW()), - ('opt-admin-email', 'default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, FALSE, TRUE, 4, NOW()), - ('opt-timezone', 'default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()), - ('opt-date-fmt', 'default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()), - ('opt-per-page', 'default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()), - ('opt-rss-items', 'default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()), - ('opt-permalink', 'default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()), - ('opt-comment-mod', 'default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, FALSE, TRUE, 20, NOW()), - ('opt-comment-order', 'default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()), - ('opt-default-role', 'default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()), - ('opt-theme', 'default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()), - ('opt-maintenance', 'default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, TRUE, TRUE, 31, NOW()) -ON CONFLICT (document_id) DO NOTHING; +INSERT INTO options (tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES + ('default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', TRUE, TRUE, 1, NOW()), + ('default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', TRUE, TRUE, 2, NOW()), + ('default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, TRUE, TRUE, 3, NOW()), + ('default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, FALSE, TRUE, 4, NOW()), + ('default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', TRUE, TRUE, 5, NOW()), + ('default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', TRUE, TRUE, 6, NOW()), + ('default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 10, NOW()), + ('default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', TRUE, TRUE, 11, NOW()), + ('default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', TRUE, TRUE, 12, NOW()), + ('default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, FALSE, TRUE, 20, NOW()), + ('default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', TRUE, TRUE, 21, NOW()), + ('default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', FALSE, TRUE, 22, NOW()), + ('default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', TRUE, TRUE, 30, NOW()), + ('default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, TRUE, TRUE, 31, NOW()) +ON CONFLICT (option_key) DO NOTHING; diff --git a/migrations/sqlite/schema.sqlite.sql b/migrations/sqlite/schema.sqlite.sql index 499400c9..3e82b273 100644 --- a/migrations/sqlite/schema.sqlite.sql +++ b/migrations/sqlite/schema.sqlite.sql @@ -9,7 +9,6 @@ -- 租户表 CREATE TABLE IF NOT EXISTS tenants ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, domain TEXT UNIQUE, config TEXT NOT NULL DEFAULT '{}', @@ -19,13 +18,12 @@ CREATE TABLE IF NOT EXISTS tenants ( ); -- 默认租户 -INSERT OR IGNORE INTO tenants (document_id, name, domain, config, status, created_at, updated_at) VALUES - ('default', 'Default', NULL, '{}', 'active', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO tenants (name, domain, config, status, created_at, updated_at) VALUES + ('Default', NULL, '{}', 'active', strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- 用户 CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', username TEXT UNIQUE NOT NULL, role TEXT NOT NULL DEFAULT 'reader', @@ -50,7 +48,6 @@ CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id); -- 用户凭据 CREATE TABLE IF NOT EXISTS user_credentials ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), auth_type TEXT NOT NULL, identifier TEXT NOT NULL, @@ -68,7 +65,6 @@ CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(auth_ty -- OAuth 账号绑定 CREATE TABLE IF NOT EXISTS oauth_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), provider TEXT NOT NULL, provider_user_id TEXT NOT NULL, @@ -90,7 +86,6 @@ CREATE INDEX IF NOT EXISTS idx_oauth_accounts_provider ON oauth_accounts(provide -- OAuth 短期 state 存储(PKCE) CREATE TABLE IF NOT EXISTS oauth_states ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, provider TEXT NOT NULL, code_verifier TEXT NOT NULL, user_id INTEGER, @@ -103,7 +98,6 @@ CREATE INDEX IF NOT EXISTS idx_oauth_states_expires ON oauth_states(expires_at); -- 币种配置 CREATE TABLE IF NOT EXISTS currencies ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, code TEXT NOT NULL UNIQUE CHECK(code = UPPER(code) AND LENGTH(code) BETWEEN 1 AND 10), name TEXT NOT NULL, decimals INTEGER NOT NULL DEFAULT 0 CHECK(decimals BETWEEN 0 AND 18), @@ -118,7 +112,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_currencies_code ON currencies(code); -- 用户钱包(每用户每币种一个) CREATE TABLE IF NOT EXISTS wallets ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), currency TEXT NOT NULL, @@ -137,7 +130,6 @@ CREATE INDEX IF NOT EXISTS idx_wallets_tenant ON wallets(tenant_id); -- 不可变交易流水 CREATE TABLE IF NOT EXISTS wallet_transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', wallet_id INTEGER NOT NULL REFERENCES wallets(id), user_id INTEGER NOT NULL REFERENCES users(id), @@ -166,7 +158,6 @@ CREATE INDEX IF NOT EXISTS idx_wallet_transactions_tenant ON wallet_transactions -- Refresh Tokens CREATE TABLE IF NOT EXISTS refresh_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), token TEXT UNIQUE NOT NULL, expires_at TEXT NOT NULL, @@ -180,7 +171,6 @@ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expir -- 站点配置 CREATE TABLE IF NOT EXISTS options ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', option_key TEXT NOT NULL, value TEXT NOT NULL, @@ -201,7 +191,6 @@ CREATE INDEX IF NOT EXISTS idx_options_tenant_option_key ON options(tenant_id, o -- RBAC 角色 CREATE TABLE IF NOT EXISTS roles ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL UNIQUE, description TEXT, @@ -215,7 +204,6 @@ CREATE INDEX IF NOT EXISTS idx_roles_tenant ON roles(tenant_id); -- RBAC 权限 CREATE TABLE IF NOT EXISTS permissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', role_id INTEGER NOT NULL REFERENCES roles(id), action TEXT NOT NULL, @@ -233,7 +221,6 @@ CREATE INDEX IF NOT EXISTS idx_permissions_tenant ON permissions(tenant_id); -- 审计日志 CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', actor_id INTEGER, actor_role TEXT, @@ -255,7 +242,6 @@ CREATE INDEX IF NOT EXISTS idx_audit_log_tenant ON audit_log(tenant_id); -- API Token CREATE TABLE IF NOT EXISTS api_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), name TEXT NOT NULL, token_hash TEXT UNIQUE NOT NULL, @@ -272,7 +258,6 @@ CREATE INDEX IF NOT EXISTS idx_api_tokens_token_hash ON api_tokens(token_hash); -- Webhook 订阅 CREATE TABLE IF NOT EXISTS webhook_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', url TEXT NOT NULL, secret TEXT NOT NULL, @@ -301,9 +286,8 @@ CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin ON plugin_storage(plugin_id -- 内容版本历史 CREATE TABLE IF NOT EXISTS content_revisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, content_type TEXT NOT NULL, - record_id TEXT NOT NULL, + record_id INTEGER NOT NULL, revision_number INTEGER NOT NULL, snapshot TEXT NOT NULL, created_by INTEGER, @@ -319,7 +303,6 @@ CREATE INDEX IF NOT EXISTS idx_revisions_ct_record_rev -- 密码重置令牌 CREATE TABLE IF NOT EXISTS password_reset_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), token TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, @@ -334,7 +317,6 @@ CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at ON password_rese -- 短信验证码 CREATE TABLE IF NOT EXISTS sms_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, phone TEXT NOT NULL, code TEXT NOT NULL, purpose TEXT NOT NULL, @@ -351,7 +333,6 @@ CREATE INDEX IF NOT EXISTS idx_sms_codes_expires ON sms_codes(expires_at); -- 邮箱验证令牌 CREATE TABLE IF NOT EXISTS email_verification_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, user_id INTEGER NOT NULL REFERENCES users(id), token TEXT NOT NULL UNIQUE, email TEXT NOT NULL, @@ -367,7 +348,6 @@ CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_expires ON email_verifi -- 后台任务队列 CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, job_type TEXT NOT NULL, payload TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', @@ -386,7 +366,6 @@ CREATE INDEX IF NOT EXISTS idx_jobs_type ON jobs(job_type); -- 定时任务调度 CREATE TABLE IF NOT EXISTS cron_schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, label TEXT NOT NULL, job_type TEXT NOT NULL, payload TEXT, @@ -406,7 +385,6 @@ CREATE INDEX IF NOT EXISTS idx_cron_plugin ON cron_schedules(plugin_id); -- Cron 执行历史 CREATE TABLE IF NOT EXISTS cron_execution_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, schedule_id INTEGER NOT NULL, job_type TEXT NOT NULL, label TEXT NOT NULL, @@ -426,7 +404,6 @@ CREATE INDEX IF NOT EXISTS idx_cron_log_started ON cron_execution_log(started_at -- 分类 CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT UNIQUE NOT NULL, slug TEXT UNIQUE NOT NULL, @@ -450,7 +427,6 @@ CREATE INDEX IF NOT EXISTS idx_categories_tenant ON categories(tenant_id); -- 标签 CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT UNIQUE NOT NULL, slug TEXT UNIQUE NOT NULL, @@ -472,7 +448,6 @@ CREATE INDEX IF NOT EXISTS idx_tags_tenant ON tags(tenant_id); -- 文章 CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', title TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, @@ -526,7 +501,6 @@ CREATE INDEX IF NOT EXISTS idx_posts_tags_tag_id ON posts_tags(tag_id); -- 评论 CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', post_id INTEGER NOT NULL REFERENCES posts(id), created_by INTEGER REFERENCES users(id), @@ -554,7 +528,6 @@ CREATE INDEX IF NOT EXISTS idx_comments_tenant ON comments(tenant_id); CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, @@ -589,7 +562,6 @@ CREATE INDEX IF NOT EXISTS idx_pages_tenant_status ON pages(tenant_id, status); CREATE TABLE IF NOT EXISTS reusable_blocks ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, block_type TEXT NOT NULL, @@ -607,7 +579,6 @@ CREATE INDEX IF NOT EXISTS idx_reusable_blocks_tenant ON reusable_blocks(tenant_ CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), filename TEXT NOT NULL, @@ -632,7 +603,6 @@ CREATE INDEX IF NOT EXISTS idx_media_tenant ON media(tenant_id); CREATE TABLE IF NOT EXISTS workflow_definitions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT, steps TEXT NOT NULL, @@ -645,7 +615,6 @@ CREATE TABLE IF NOT EXISTS workflow_definitions ( CREATE TABLE IF NOT EXISTS workflow_instances ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, definition_id INTEGER NOT NULL REFERENCES workflow_definitions(id), status TEXT NOT NULL DEFAULT 'running', current_step TEXT, @@ -661,7 +630,6 @@ CREATE INDEX IF NOT EXISTS idx_wf_instances_status ON workflow_instances(status) CREATE TABLE IF NOT EXISTS workflow_step_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, instance_id INTEGER NOT NULL REFERENCES workflow_instances(id), step_id TEXT NOT NULL, step_name TEXT NOT NULL, @@ -680,53 +648,52 @@ CREATE INDEX IF NOT EXISTS idx_wf_step_logs_instance ON workflow_step_logs(insta -- ============================================================ -- 系统角色 -INSERT OR IGNORE INTO roles (document_id, tenant_id, name, description, is_system, created_at, updated_at) VALUES - ('role-admin', 'default', 'admin', '超级管理员', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('role-editor', 'default', 'editor', '编辑', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('role-author', 'default', 'author', '作者', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('role-reader', 'default', 'reader', '读者', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO roles (tenant_id, name, description, is_system, created_at, updated_at) VALUES + ('default', 'admin', '超级管理员', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'editor', '编辑', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'author', '作者', 0, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'reader', '读者', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- admin 全局权限 -INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-admin-all', 'default', (SELECT id FROM roles WHERE document_id = 'role-admin'), '*', '*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'admin'), '*', '*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- editor 权限 -INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-editor-ct-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'editor'), 'content-type::*.*', 'content-type::*', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- author 权限 -INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-author-post-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('perm-author-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('perm-author-post-update', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('perm-author-post-delete', 'default', (SELECT id FROM roles WHERE document_id = 'role-author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.create', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.read', 'content-type::post', '["*"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.update', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', (SELECT id FROM roles WHERE name = 'author'), 'content-type::post.delete', 'content-type::post', '["*"]', '{"author_id":"$user.id"}', strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- reader 权限 -INSERT OR IGNORE INTO permissions (document_id, tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES - ('perm-reader-post-read', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('perm-reader-comment-create', 'default', (SELECT id FROM roles WHERE document_id = 'role-reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO permissions (tenant_id, role_id, action, subject, fields, conditions, created_at) VALUES + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::post.read', 'content-type::post', '["title","slug","content","excerpt","status"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', (SELECT id FROM roles WHERE name = 'reader'), 'content-type::comment.create', 'content-type::comment', '["content","nickname","email"]', NULL, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- 站点配置 -INSERT OR IGNORE INTO options (document_id, tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES - ('opt-site-title', 'default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', 1, 1, 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-site-desc', 'default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', 1, 1, 2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-site-url', 'default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, 1, 1, 3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-admin-email', 'default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, 0, 1, 4, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-timezone', 'default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', 1, 1, 5, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-date-fmt', 'default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', 1, 1, 6, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-per-page', 'default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', 1, 1, 10, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-rss-items', 'default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', 1, 1, 11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-permalink', 'default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', 1, 1, 12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-comment-mod', 'default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, 0, 1, 20, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-comment-order', 'default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', 1, 1, 21, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-default-role', 'default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', 0, 1, 22, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-theme', 'default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', 1, 1, 30, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), - ('opt-maintenance', 'default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, 1, 1, 31, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); +INSERT OR IGNORE INTO options (tenant_id, option_key, value, type, group_name, label, description, validation, is_public, autoload, sort_order, updated_at) VALUES + ('default', 'site_title', '"My Blog"', 'text', 'general', '站点标题', '显示在浏览器标题栏和页面头部', '{"max_length":100}', 1, 1, 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'site_description', '""', 'text', 'general', '站点描述', '简短描述站点用途', '{"max_length":500}', 1, 1, 2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'site_url', '""', 'url', 'general', '站点 URL', '如 https://example.com', NULL, 1, 1, 3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'admin_email', '""', 'email', 'general', '管理员邮箱', NULL, NULL, 0, 1, 4, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'timezone', '"UTC"', 'select', 'general', '时区', NULL, '{"values":["UTC","Asia/Shanghai","Asia/Tokyo","US/Eastern","US/Pacific","Europe/London","Europe/Berlin"]}', 1, 1, 5, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'date_format', '"%Y-%m-%d"', 'select', 'general', '日期格式', NULL, '{"values":["%Y-%m-%d","%d/%m/%Y","%m/%d/%Y","%Y年%m月%d日"]}', 1, 1, 6, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'posts_per_page', '10', 'integer', 'reading', '每页文章数', NULL, '{"min":1,"max":100}', 1, 1, 10, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'rss_items', '20', 'integer', 'reading', 'RSS 条目数', NULL, '{"min":1,"max":100}', 1, 1, 11, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'permalink_structure', '"/:year/:month/:slug"', 'select', 'reading', 'URL 结构', NULL, '{"values":["/:year/:month/:slug","/:slug","/posts/:slug"]}', 1, 1, 12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'comment_moderation', 'true', 'boolean', 'discussion', '评论需审核', '开启后新评论需管理员审批', NULL, 0, 1, 20, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'comment_order', '"asc"', 'select', 'discussion', '评论排序', NULL, '{"values":["asc","desc"]}', 1, 1, 21, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'default_role', '"reader"', 'select', 'discussion', '新用户默认角色', NULL, '{"values":["reader","author"]}', 0, 1, 22, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'theme', '"default"', 'select', 'appearance', '当前主题', NULL, '{"values":["default","corporate","minimal","warm"]}', 1, 1, 30, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + ('default', 'maintenance_mode', 'false', 'boolean', 'appearance', '维护模式', '开启后前台显示维护页面', NULL, 1, 1, 31, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')); -- Products CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', category_id INTEGER REFERENCES categories(id), title TEXT NOT NULL, @@ -772,7 +739,6 @@ CREATE INDEX IF NOT EXISTS idx_products_tenant ON products(tenant_id); -- Product Variants CREATE TABLE IF NOT EXISTS product_variants ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', product_id INTEGER NOT NULL REFERENCES products(id), sku TEXT UNIQUE, @@ -794,7 +760,6 @@ CREATE INDEX IF NOT EXISTS idx_product_variants_tenant ON product_variants(tenan -- User Addresses CREATE TABLE IF NOT EXISTS user_addresses ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), label TEXT NOT NULL DEFAULT '', @@ -819,7 +784,6 @@ CREATE INDEX IF NOT EXISTS idx_user_addresses_tenant ON user_addresses(tenant_id -- Orders CREATE TABLE IF NOT EXISTS orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), order_no TEXT NOT NULL UNIQUE, @@ -860,7 +824,6 @@ CREATE INDEX IF NOT EXISTS idx_orders_tenant ON orders(tenant_id); -- Order Items CREATE TABLE IF NOT EXISTS order_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', order_id INTEGER NOT NULL REFERENCES orders(id), product_id INTEGER REFERENCES products(id), @@ -882,7 +845,6 @@ CREATE INDEX IF NOT EXISTS idx_order_items_tenant ON order_items(tenant_id); CREATE TABLE IF NOT EXISTS cart_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), product_id INTEGER NOT NULL REFERENCES products(id), @@ -897,7 +859,6 @@ CREATE INDEX IF NOT EXISTS idx_cart_items_tenant ON cart_items(tenant_id); CREATE TABLE IF NOT EXISTS payment_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', provider TEXT NOT NULL, name TEXT NOT NULL, @@ -920,7 +881,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_channels_tenant ON payment_channels(tenan -- Payment Orders CREATE TABLE IF NOT EXISTS payment_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL REFERENCES users(id), order_id TEXT, @@ -960,7 +920,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_orders_tenant ON payment_orders(tenant_id -- Payment Transactions CREATE TABLE IF NOT EXISTS payment_transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', payment_order_id INTEGER NOT NULL REFERENCES payment_orders(id), order_id TEXT, @@ -981,7 +940,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_transactions_tenant ON payment_transactio -- Payment Refunds CREATE TABLE IF NOT EXISTS payment_refunds ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', payment_order_id INTEGER NOT NULL REFERENCES payment_orders(id), order_id TEXT, @@ -1004,7 +962,6 @@ CREATE INDEX IF NOT EXISTS idx_payment_refunds_tenant ON payment_refunds(tenant_ -- Wallet Outbox (ensures wallet operations are never lost) CREATE TABLE IF NOT EXISTS wallet_outbox ( id INTEGER PRIMARY KEY AUTOINCREMENT, - document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', user_id INTEGER NOT NULL, currency TEXT NOT NULL, diff --git a/src/aspects/engine.rs b/src/aspects/engine.rs index 805a9576..9c5069b2 100644 --- a/src/aspects/engine.rs +++ b/src/aspects/engine.rs @@ -552,10 +552,11 @@ fn matches_table_any(pointcuts: &[Pointcut], table: &str) -> bool { fn make_base_ctx(auth: &crate::middleware::auth::AuthUser) -> super::BaseContext { super::BaseContext::new( - auth.user_id().map(|s| s.to_string()), + auth.user_id().map(|id| id.to_string()), auth.tenant_id().unwrap_or(DEFAULT_TENANT).to_string(), crate::utils::tz::now_str(), ) + .with_user_int_id(auth.user_id()) } fn merge_non_null(mut record: super::Record, data: &impl serde::Serialize) -> super::Record { diff --git a/src/bin/export-types.rs b/src/bin/export-types.rs index 400eceee..6918ec38 100644 --- a/src/bin/export-types.rs +++ b/src/bin/export-types.rs @@ -71,8 +71,6 @@ fn main() { handlers::page::ReorderRequest, handlers::page::ReorderItem, handlers::page::SitemapEntry, - handlers::post::PostListQuery, - handlers::post::AdminPostListQuery, handlers::comment::AdminCommentListQuery, workflow::handler::CreateWorkflowRequest, workflow::handler::StartWorkflowRequest, @@ -131,7 +129,7 @@ fn main() { plugins::PluginInfoResponse, plugins::Permissions, webhook::model::WebhookSubscription, - audit::model::AuditEntry, + models::audit_log::AuditEntry, worker::CronSchedule, worker::CronExecutionLog, services::options::OptionGroup, diff --git a/src/cli.rs b/src/cli.rs index 145d575a..c6d51601 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,7 @@ //! Uses clap derive to define the command-line structure, dispatching each subcommand to its module. mod app_cmd; +mod codegen_cmd; mod ct_cmd; mod db_cmd; mod doctor_cmd; @@ -55,6 +56,11 @@ enum Commands { }, /// System diagnostics Doctor, + /// Code generation + Codegen { + #[command(subcommand)] + action: CodegenAction, + }, /// Proxy management (multi-tenant reverse proxy) #[cfg(feature = "proxy")] Proxy { @@ -167,6 +173,21 @@ pub enum PluginAction { }, } +#[derive(Subcommand)] +pub enum CodegenAction { + /// Generate model scaffold from schema.sql + Model { + /// Table names to generate (omit to generate all) + tables: Vec, + /// Overwrite existing files + #[arg(long)] + force: bool, + /// Print generated code without writing files + #[arg(long)] + dry_run: bool, + }, +} + fn lerp(a: u8, b: u8, t: f32) -> u8 { (a as f32 + (b as f32 - a as f32) * t) as u8 } @@ -401,6 +422,17 @@ pub async fn run(cli: Cli, config: &AppConfig) -> anyhow::Result<()> { doctor_cmd::run(config).await; } + Some(Commands::Codegen { + action: + CodegenAction::Model { + tables, + force, + dry_run, + }, + }) => { + codegen_cmd::run_model(&tables, force, dry_run)?; + } + #[cfg(feature = "proxy")] Some(Commands::Proxy { action: ProxyAction::Start { diff --git a/src/cli/codegen_cmd.rs b/src/cli/codegen_cmd.rs new file mode 100644 index 00000000..f6184c12 --- /dev/null +++ b/src/cli/codegen_cmd.rs @@ -0,0 +1,278 @@ +//! Code generation commands. +//! +//! Parses `schema.sqlite.sql` with `sqlparser` and generates model `.rs` files. + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use sqlparser::ast::{ColumnDef, ColumnOption, DataType, Statement, TableConstraint}; +use sqlparser::dialect::GenericDialect; +use sqlparser::parser::Parser; + +use serde::Serialize; + +// ── Data structures ────────────────────────────────────────────────── + +#[derive(Serialize)] +struct Column { + name: String, + rust_type: String, +} + +#[derive(Serialize)] +struct Table { + name: String, + struct_name: String, + columns: Vec, + has_tenant_id: bool, + has_timestamp: bool, + singular: String, +} + +// ── CLI entry ──────────────────────────────────────────────────────── + +pub fn run_model(tables: &[String], force: bool, dry_run: bool) -> anyhow::Result<()> { + let schema_path = find_schema()?; + let sql = fs::read_to_string(&schema_path) + .map_err(|e| anyhow::anyhow!("failed to read {}: {e}", schema_path.display()))?; + + let parsed = parse_tables(&sql)?; + + let targets: Vec<&Table> = if tables.is_empty() { + parsed.values().collect() + } else { + tables + .iter() + .map(|t| { + let key = t.to_lowercase(); + parsed + .values() + .find(|tbl| tbl.name == key || tbl.struct_name.to_lowercase() == key) + .ok_or_else(|| anyhow::anyhow!("table not found: {t}")) + }) + .collect::>()? + }; + + let models_dir = find_models_dir()?; + + let mut tera = tera::Tera::default(); + tera.add_raw_template( + "model.rs", + include_str!("../../templates/codegen/model.rs.tera"), + )?; + + for table in &targets { + let ctx = tera::Context::from_serialize(table)?; + let code = tera.render("model.rs", &ctx)?; + + let file_path = models_dir.join(format!("{}.rs", table.name)); + + if dry_run { + println!("── {} ──", file_path.display()); + println!("{code}"); + println!(); + continue; + } + + if file_path.exists() && !force { + println!( + "SKIP {} (exists, use --force to overwrite)", + file_path.display() + ); + continue; + } + + fs::write(&file_path, &code) + .map_err(|e| anyhow::anyhow!("failed to write {}: {e}", file_path.display()))?; + println!("WROTE {}", file_path.display()); + } + + Ok(()) +} + +// ── Schema discovery ───────────────────────────────────────────────── + +fn find_schema() -> anyhow::Result { + let candidates = [ + "migrations/sqlite/schema.sqlite.sql", + "schema.sqlite.sql", + "migrations/schema.sqlite.sql", + ]; + for c in &candidates { + if Path::new(c).exists() { + return Ok(PathBuf::from(c)); + } + } + Err(anyhow::anyhow!( + "schema.sqlite.sql not found (searched: {})", + candidates.join(", ") + )) +} + +fn find_models_dir() -> anyhow::Result { + let dir = Path::new("src/models"); + if dir.exists() { + return Ok(dir.to_path_buf()); + } + Err(anyhow::anyhow!("src/models/ directory not found")) +} + +// ── SQL parsing ────────────────────────────────────────────────────── + +fn parse_tables(sql: &str) -> anyhow::Result> { + let dialect = GenericDialect; + let statements = + Parser::parse_sql(&dialect, sql).map_err(|e| anyhow::anyhow!("SQL parse error: {e}"))?; + + let mut tables = BTreeMap::new(); + + for stmt in &statements { + let Statement::CreateTable(ct) = stmt else { + continue; + }; + + let table_name = ct + .name + .0 + .first() + .and_then(|p| match p { + sqlparser::ast::ObjectNamePart::Identifier(i) => Some(i.value.to_lowercase()), + _ => None, + }) + .unwrap_or_default(); + + if table_name.is_empty() { + continue; + } + + let _pk_columns = collect_pk_columns(&ct.columns, &ct.constraints); + let has_tenant_id = ct.columns.iter().any(|c| c.name.value == "tenant_id"); + let has_timestamp = ct.columns.iter().any(|c| { + let n = c.name.value.as_str(); + (n == "created_at" || n == "updated_at") && matches!(c.data_type, DataType::Text) + }); + + let columns: Vec = ct + .columns + .iter() + .map(|c| { + let col_name = &c.name.value; + let is_nullable = is_column_nullable(c); + let rust_type = if col_name == "tenant_id" { + "Option".to_string() + } else { + map_type(&c.data_type, is_nullable, col_name) + }; + Column { + name: col_name.clone(), + rust_type, + } + }) + .collect(); + + let struct_name = singularize(&to_pascal_case(&table_name)); + let singular = singularize(&table_name); + + tables.insert( + table_name.clone(), + Table { + name: table_name, + struct_name, + columns, + has_tenant_id, + has_timestamp, + singular, + }, + ); + } + + Ok(tables) +} + +fn collect_pk_columns(columns: &[ColumnDef], constraints: &[TableConstraint]) -> Vec { + let mut pks = Vec::new(); + + for c in columns { + for opt in &c.options { + if let ColumnOption::PrimaryKey(_) = opt.option { + pks.push(c.name.value.clone()); + } + } + } + + for c in constraints { + if let TableConstraint::PrimaryKey(pk) = c { + for ic in &pk.columns { + if let sqlparser::ast::Expr::Identifier(ident) = &ic.column.expr { + pks.push(ident.value.clone()); + } + } + } + } + + pks +} + +fn is_column_nullable(col: &ColumnDef) -> bool { + for opt in &col.options { + match &opt.option { + ColumnOption::NotNull => return false, + ColumnOption::PrimaryKey(_) => return false, + _ => {} + } + } + true +} + +fn map_type(data_type: &DataType, nullable: bool, col_name: &str) -> String { + let base = match data_type { + DataType::Int(_) | DataType::Integer(_) | DataType::SmallInt(_) | DataType::BigInt(_) => { + "i64" + } + DataType::Boolean => "bool", + DataType::Real | DataType::Float(_) | DataType::Double(_) => "f64", + DataType::Text | DataType::Varchar(_) | DataType::Char(_) | DataType::String(_) => { + if col_name == "created_at" || col_name == "updated_at" { + "Timestamp" + } else { + "String" + } + } + DataType::Blob(_) => "Vec", + _ => "String", + }; + + if nullable { + format!("Option<{base}>") + } else { + base.to_string() + } +} + +// ── Helpers ────────────────────────────────────────────────────────── + +fn to_pascal_case(snake: &str) -> String { + snake + .split('_') + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect() +} + +fn singularize(word: &str) -> String { + if word.ends_with("ies") && word.len() > 3 { + format!("{}y", &word[..word.len() - 3]) + } else if word.ends_with("ses") || word.ends_with("xes") || word.ends_with("zes") { + word[..word.len() - 2].to_string() + } else if word.ends_with('s') && !word.ends_with("ss") && word.len() > 1 { + word[..word.len() - 1].to_string() + } else { + word.to_string() + } +} diff --git a/src/cli/db_cmd.rs b/src/cli/db_cmd.rs index 0815ac42..3d1911d5 100644 --- a/src/cli/db_cmd.rs +++ b/src/cli/db_cmd.rs @@ -58,7 +58,7 @@ pub async fn seed( let password_hash = raisfast::services::auth::hash_password(password) .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))?; - let (document_id, now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (id, now) = raisfast::utils::id::new_id_and_timestamp(); let tid = if cfg!(feature = "db-sqlite") { let row: Option<(String,)> = @@ -73,7 +73,7 @@ pub async fn seed( match tid { Some(tid) => { sqlx::query(&format!( - "INSERT INTO users (document_id, tenant_id, username, created_at, updated_at, role, status, registered_via) VALUES ({}, {}, {}, {}, {}, {}, {}, {})", + "INSERT INTO users (id, tenant_id, username, created_at, updated_at, role, status, registered_via) VALUES ({}, {}, {}, {}, {}, {}, {}, {})", dialect::ph(1), dialect::ph(2), dialect::ph(3), @@ -83,7 +83,7 @@ pub async fn seed( dialect::ph(7), dialect::ph(8), )) - .bind(&document_id) + .bind(id) .bind(&tid) .bind(username) .bind(now) @@ -96,7 +96,7 @@ pub async fn seed( } None => { sqlx::query(&format!( - "INSERT INTO users (document_id, username, created_at, updated_at, role, status, registered_via) VALUES ({}, {}, {}, {}, {}, {}, {})", + "INSERT INTO users (id, username, created_at, updated_at, role, status, registered_via) VALUES ({}, {}, {}, {}, {}, {}, {})", dialect::ph(1), dialect::ph(2), dialect::ph(3), @@ -105,7 +105,7 @@ pub async fn seed( dialect::ph(6), dialect::ph(7), )) - .bind(&document_id) + .bind(id) .bind(username) .bind(now) .bind(now) @@ -118,17 +118,17 @@ pub async fn seed( } let (user_id,): (i64,) = sqlx::query_as(&format!( - "SELECT id FROM users WHERE document_id = {}", + "SELECT id FROM users WHERE id = {}", dialect::ph(1) )) - .bind(&document_id) + .bind(id) .fetch_one(&pool) .await?; let cred_data = serde_json::json!({"password_hash": password_hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); sqlx::query(&format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, 1, {}, {})", dialect::ph(1), dialect::ph(2), dialect::ph(3), @@ -137,7 +137,7 @@ pub async fn seed( dialect::ph(6), dialect::ph(7), )) - .bind(&cred_doc_id) + .bind(cred_id) .bind(user_id) .bind("email") .bind(email) diff --git a/src/commands/page.rs b/src/commands/page.rs index 6088e163..205d0449 100644 --- a/src/commands/page.rs +++ b/src/commands/page.rs @@ -24,6 +24,7 @@ pub struct CreatePageCmd { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdatePageCmd { + #[serde(serialize_with = "crate::utils::id::serialize_id_as_string")] pub id: i64, pub title: Option, pub slug: Option, diff --git a/src/constants.rs b/src/constants.rs index b1843f14..b2aad815 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -44,8 +44,6 @@ pub const COL_META: &str = "__meta"; pub const COL_TENANT_ID: &str = "tenant_id"; /// Primary key column pub const COL_ID: &str = "id"; -pub const COL_DOCUMENT_ID: &str = "document_id"; - // ─── Auth Header ─── pub const HEADER_AUTHORIZATION: &str = "authorization"; diff --git a/src/content_type/handler.rs b/src/content_type/handler.rs index a1644b70..fc692d4a 100644 --- a/src/content_type/handler.rs +++ b/src/content_type/handler.rs @@ -13,9 +13,7 @@ use serde_json::{Value, json}; use std::collections::HashMap; use std::sync::Arc; -use super::repository::{ - ContentQuery, ContentRepository, SaveContext, resolve_document_id_to_int_id, -}; +use super::repository::{ContentQuery, ContentRepository, SaveContext, find_existing_id}; use super::rule_engine::compile_rule_sql; use super::schema::{ContentKind, ContentTypeSchema, FieldType, RelationType, check_api_access}; use crate::AppState; @@ -98,25 +96,17 @@ pub fn routes( r } -fn looks_like_uuid(s: &str) -> bool { - s.len() == 36 - && s.as_bytes().iter().enumerate().all(|(i, c)| match i { - 8 | 13 | 18 | 23 => *c == b'-', - _ => c.is_ascii_hexdigit(), - }) -} - fn make_base_ctx_from_auth( auth: &AuthUser, pool: &crate::db::pool::Pool, ) -> crate::aspects::BaseContext { crate::aspects::BaseContext::new( - auth.user_id().map(|s| s.to_string()), + auth.user_id().map(|id| id.to_string()), auth.tenant_id().unwrap_or(DEFAULT_TENANT).to_string(), crate::utils::tz::now_str(), ) .with_pool(pool.clone()) - .with_user_int_id(auth.user_int_id()) + .with_user_int_id(auth.user_id()) } fn make_base_ctx(state: &AppState, save_ctx: &SaveContext) -> crate::aspects::BaseContext { @@ -260,7 +250,7 @@ pub fn register_content_routes( }), ) .route( - &format!("{cms}/{plural}/{{id_or_slug}}"), + &format!("{cms}/{plural}/{{id}}"), axum::routing::get({ let singular = singular.clone(); move |auth, state, path| get_handler(auth, state, path, singular.clone()) @@ -284,7 +274,7 @@ pub fn register_content_routes( }), ) .route( - &format!("{admin_cms}/{plural}/{{id_or_slug}}"), + &format!("{admin_cms}/{plural}/{{id}}"), axum::routing::get({ let singular = singular.clone(); move |state, path| admin_get_handler(state, path, singular.clone()) @@ -309,14 +299,14 @@ pub fn register_content_routes( }), ) .route( - &format!("{cms}/{plural}/{{id_or_slug}}"), + &format!("{cms}/{plural}/{{id}}"), axum::routing::get({ let singular = singular.clone(); move |auth, state, path| get_handler(auth, state, path, singular.clone()) }), ) .route( - &format!("{cms}/{plural}/{{id_or_slug}}/update"), + &format!("{cms}/{plural}/{{id}}/update"), axum::routing::post({ let singular = singular.clone(); move |auth, state, path, data| { @@ -325,7 +315,7 @@ pub fn register_content_routes( }), ) .route( - &format!("{cms}/{plural}/{{id_or_slug}}/delete"), + &format!("{cms}/{plural}/{{id}}/delete"), axum::routing::post({ let singular = singular.clone(); move |auth, state, path| delete_handler(auth, state, path, singular.clone()) @@ -339,7 +329,7 @@ pub fn register_content_routes( }), ) .route( - &format!("{admin_cms}/{plural}/{{id_or_slug}}"), + &format!("{admin_cms}/{plural}/{{id}}"), axum::routing::get({ let singular = singular.clone(); move |state, path| admin_get_handler(state, path, singular.clone()) @@ -357,7 +347,7 @@ pub fn register_content_routes( api } -/// Parse catch-all path into (plural, optional id_or_slug) +/// Parse catch-all path into (plural, optional id) fn parse_dynamic_path(path: &str) -> Option<(String, Option)> { let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); if segments.is_empty() { @@ -384,7 +374,7 @@ fn resolve_content_type( None } -/// Parse catch-all path into (segment, optional id_or_slug, optional action) +/// Parse catch-all path into (segment, optional id, optional action) fn parse_dynamic_path_with_action(path: &str) -> Option<(String, Option, Option)> { let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); if segments.is_empty() { @@ -484,19 +474,22 @@ async fn dynamic_cms_dispatch_restful( } (axum::http::Method::GET, Some(id)) => { check_api_access(ct.api.get.access, &auth)?; - let data = do_get(state, &ct, &id, &auth).await?; + let int_id = crate::utils::id::parse_id(&id)?; + let data = do_get(state, &ct, int_id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } (axum::http::Method::PUT, Some(id)) => { check_api_access(ct.api.update.access, &auth)?; + let int_id = crate::utils::id::parse_id(&id)?; let Json(data) = body.ok_or_else(|| AppError::BadRequest("body required".into()))?; - let result = do_update(state, &ct, &id, data, &save_ctx, &auth).await?; + let result = do_update(state, &ct, int_id, data, &save_ctx, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(result)).into_response()) } (axum::http::Method::DELETE, Some(id)) => { check_api_access(ct.api.delete.access, &auth)?; - do_delete(state, &ct, &id, &auth).await?; + let int_id = crate::utils::id::parse_id(&id)?; + do_delete(state, &ct, int_id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success( json!({"deleted": true}), )) @@ -561,19 +554,22 @@ async fn dynamic_cms_dispatch_simple( } (axum::http::Method::GET, Some(id), None) => { check_api_access(ct.api.get.access, &auth)?; - let data = do_get(state, &ct, &id, &auth).await?; + let int_id = crate::utils::id::parse_id(&id)?; + let data = do_get(state, &ct, int_id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } (axum::http::Method::POST, Some(id), Some("update")) => { check_api_access(ct.api.update.access, &auth)?; + let int_id = crate::utils::id::parse_id(&id)?; let Json(data) = body.ok_or_else(|| AppError::BadRequest("body required".into()))?; - let result = do_update(state, &ct, &id, data, &save_ctx, &auth).await?; + let result = do_update(state, &ct, int_id, data, &save_ctx, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(result)).into_response()) } (axum::http::Method::POST, Some(id), Some("delete")) => { check_api_access(ct.api.delete.access, &auth)?; - do_delete(state, &ct, &id, &auth).await?; + let int_id = crate::utils::id::parse_id(&id)?; + do_delete(state, &ct, int_id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success( json!({"deleted": true}), )) @@ -629,7 +625,8 @@ async fn dynamic_admin_cms_dispatch_restful( Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } (axum::http::Method::GET, Some(id)) => { - let data = do_admin_get(state, &ct, &id).await?; + let int_id = crate::utils::id::parse_id(&id)?; + let data = do_admin_get(state, &ct, int_id).await?; Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } _ => Err(AppError::not_found(&format!("{method} {path}"))), @@ -670,7 +667,8 @@ async fn dynamic_admin_cms_dispatch_simple( Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } (axum::http::Method::GET, Some(id)) => { - let data = do_admin_get(state, &ct, &id).await?; + let int_id = crate::utils::id::parse_id(&id)?; + let data = do_admin_get(state, &ct, int_id).await?; Ok(Json(crate::errors::response::ApiResponse::success(data)).into_response()) } _ => Err(AppError::not_found(&format!("{method} {path}"))), @@ -703,8 +701,8 @@ pub fn cms_list_cache_key(ct: &ContentTypeSchema, query: &ContentQuery) -> Strin format!("cms:{}:{h:x}", ct.plural) } -pub fn cms_detail_cache_key(ct: &ContentTypeSchema, id_or_slug: &str) -> String { - format!("cms:{}:detail:{id_or_slug}", ct.plural) +pub fn cms_detail_cache_key(ct: &ContentTypeSchema, id: i64) -> String { + format!("cms:{}:detail:{id}", ct.plural) } fn invalidate_cms_cache(state: &AppState, ct: &ContentTypeSchema) { @@ -749,7 +747,8 @@ pub async fn do_list( .foreign_key .clone() .unwrap_or_else(|| format!("{}_id", field.name)); - let int_id = resolve_document_id_to_int_id(&state.pool, &rel.target, v) + let parsed_id = crate::utils::id::parse_id(v).unwrap_or(-1); + let int_id = find_existing_id(&state.pool, &rel.target, parsed_id) .await .ok() .flatten() @@ -852,10 +851,10 @@ pub async fn do_list( pub async fn do_get( state: &AppState, ct: &ContentTypeSchema, - id_or_slug: &str, + id: i64, auth: &AuthUser, ) -> Result { - let cache_key = cms_detail_cache_key(ct, id_or_slug); + let cache_key = cms_detail_cache_key(ct, id); let cache_ttl = std::time::Duration::from_secs(state.config.rule_engine.cms_cache_ttl_secs); if ct.api.get.cache && let Some(entry) = state.cms_cache.get(&cache_key) @@ -865,29 +864,15 @@ pub async fn do_get( } let repo = ContentRepository::new(state.pool.clone()); - - let item = if looks_like_uuid(id_or_slug) { - repo.find_by_id(ct, id_or_slug, None, false).await? - } else { - let by_slug = if ct.slug_field.is_some() { - repo.find_by_slug(ct, id_or_slug, None, None, false).await? - } else { - None - }; - match by_slug { - Some(data) => Some(data), - None => repo.find_by_id(ct, id_or_slug, None, false).await?, - } - }; - - let result = item.ok_or_else(|| AppError::not_found(&format!("{}/{}", ct.name, id_or_slug)))?; + let item = repo.find_by_id(ct, id, None, false).await?; + let result = item.ok_or_else(|| AppError::not_found(&format!("{}/{}", ct.name, id)))?; if let Some(rules) = ct.cached_rules.as_ref() && let Some(rule) = rules.get.filter.as_ref() { let ctx = super::rule_engine::RuleContext::from_auth(auth); if !rule.evaluate(&result, &ctx, &state.config.rule_engine) { - return Err(AppError::not_found(&format!("{}/{}", ct.name, id_or_slug))); + return Err(AppError::not_found(&format!("{}/{}", ct.name, id))); } } @@ -897,7 +882,7 @@ pub async fn do_get( "on_content_viewed", &json!({ "content_type": ct.singular, - "id": result.get(COL_DOCUMENT_ID).and_then(|v| v.as_str()).unwrap_or(""), + "id": result.get(COL_ID).and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))).unwrap_or(0), }), ) .await; @@ -951,9 +936,9 @@ pub async fn do_create( invalidate_cms_cache(state, ct); let id = result - .get(COL_DOCUMENT_ID) - .and_then(|v| v.as_str()) - .unwrap_or("") + .get(COL_ID) + .and_then(|v| v.as_i64()) + .unwrap_or(0) .to_string(); let slug = result .get("slug") @@ -995,7 +980,7 @@ pub async fn do_create( pub async fn do_update( state: &AppState, ct: &ContentTypeSchema, - id: &str, + id: i64, data: Value, save_ctx: &SaveContext, auth: &AuthUser, @@ -1097,7 +1082,7 @@ pub async fn do_update( pub async fn do_delete( state: &AppState, ct: &ContentTypeSchema, - id: &str, + id: i64, auth: &AuthUser, ) -> Result<(), AppError> { let repo = ContentRepository::new(state.pool.clone()); @@ -1220,7 +1205,8 @@ async fn do_admin_list( .foreign_key .clone() .unwrap_or_else(|| format!("{}_id", field.name)); - let int_id = resolve_document_id_to_int_id(&state.pool, &rel.target, v) + let parsed_id = crate::utils::id::parse_id(v).unwrap_or(-1); + let int_id = find_existing_id(&state.pool, &rel.target, parsed_id) .await .ok() .flatten() @@ -1264,7 +1250,7 @@ async fn do_admin_list( async fn do_admin_get( state: &AppState, ct: &ContentTypeSchema, - id: &str, + id: i64, ) -> Result { let repo = ContentRepository::new(state.pool.clone()); let item = repo.find_by_id(ct, id, None, true).await?; @@ -1316,13 +1302,9 @@ pub async fn do_single_update( ) -> Result { let repo = ContentRepository::new(state.pool.clone()); let existing = repo.ensure_single(ct, None).await?; - let id = existing - .get(COL_DOCUMENT_ID) - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + let id = existing.get(COL_ID).and_then(|v| v.as_i64()).unwrap_or(0); - do_update(state, ct, &id, data, save_ctx, auth).await + do_update(state, ct, id, data, save_ctx, auth).await } async fn do_admin_single_get( @@ -1395,7 +1377,7 @@ async fn list_handler( async fn get_handler( auth: AuthUser, State(state): State, - Path(id_or_slug): Path, + Path(id): Path, type_name: String, ) -> Result { let ct = state @@ -1403,7 +1385,8 @@ async fn get_handler( .get(&type_name) .ok_or_else(|| AppError::not_found(&type_name))?; check_api_access(ct.api.get.access, &auth)?; - let data = do_get(&state, &ct, &id_or_slug, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + let data = do_get(&state, &ct, id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(data))) } @@ -1444,8 +1427,9 @@ async fn update_handler( .get(&type_name) .ok_or_else(|| AppError::not_found(&type_name))?; check_api_access(ct.api.update.access, &auth)?; + let int_id = crate::utils::id::parse_id(&id)?; let save_ctx = SaveContext::from_auth(&auth); - let result = do_update(&state, &ct, &id, data, &save_ctx, &auth).await?; + let result = do_update(&state, &ct, int_id, data, &save_ctx, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success(result))) } @@ -1460,7 +1444,8 @@ async fn delete_handler( .get(&type_name) .ok_or_else(|| AppError::not_found(&type_name))?; check_api_access(ct.api.delete.access, &auth)?; - do_delete(&state, &ct, &id, &auth).await?; + let int_id = crate::utils::id::parse_id(&id)?; + do_delete(&state, &ct, int_id, &auth).await?; Ok(Json(crate::errors::response::ApiResponse::success( json!({"deleted": true}), ))) @@ -1488,7 +1473,8 @@ async fn admin_get_handler( .content_type_registry .get(&type_name) .ok_or_else(|| AppError::not_found(&type_name))?; - let data = do_admin_get(&state, &ct, &id).await?; + let int_id = crate::utils::id::parse_id(&id)?; + let data = do_admin_get(&state, &ct, int_id).await?; Ok(Json(crate::errors::response::ApiResponse::success(data))) } @@ -1703,7 +1689,7 @@ fn filter_fields( let protocol_cols: Vec<&str> = ct.protocol_column_names(); let system_keys: Vec = obj .keys() - .filter(|k| *k == COL_ID || *k == COL_DOCUMENT_ID || protocol_cols.contains(&k.as_str())) + .filter(|k| *k == COL_ID || protocol_cols.contains(&k.as_str())) .cloned() .collect(); obj.retain(|k, _| allowed.contains(&k.to_string()) || system_keys.contains(k)); @@ -1870,13 +1856,11 @@ type = "text" #[test] fn filter_fields_with_whitelist() { let ct = parse_ct(); - let data = - json!({"title": "Hello", "price": 100, "id": 1, "document_id": "abc", "extra": "x"}); + let data = json!({"title": "Hello", "price": 100, "id": 1, "extra": "x"}); let filtered = filter_fields(data, Some(&["title".to_string()]), &ct); let obj = filtered.as_object().unwrap(); assert!(obj.contains_key("title")); assert!(obj.contains_key("id")); - assert!(obj.contains_key("document_id")); assert!(!obj.contains_key("price")); assert!(!obj.contains_key("extra")); } @@ -1933,8 +1917,8 @@ type = "text" #[test] fn cms_detail_cache_key_format() { let ct = parse_ct(); - let key = cms_detail_cache_key(&ct, "doc-123"); - assert_eq!(key, "cms:products:detail:doc-123"); + let key = cms_detail_cache_key(&ct, 123); + assert_eq!(key, "cms:products:detail:123"); } #[test] diff --git a/src/content_type/migration.rs b/src/content_type/migration.rs index 3170587d..6436b888 100644 --- a/src/content_type/migration.rs +++ b/src/content_type/migration.rs @@ -5,7 +5,7 @@ use super::schema::{ContentTypeSchema, FieldType, RelationType}; use crate::aspects::ColumnDef; -use crate::constants::{COL_DOCUMENT_ID, COL_ID}; +use crate::constants::COL_ID; /// Generate CREATE TABLE SQL from a content type definition /// @@ -19,8 +19,7 @@ pub fn generate_create_table(ct: &ContentTypeSchema, protocol_columns: &[ColumnD } let mut cols = Vec::new(); - cols.push(format!(" {COL_ID} INTEGER PRIMARY KEY AUTOINCREMENT")); - cols.push(format!(" {COL_DOCUMENT_ID} TEXT NOT NULL UNIQUE")); + cols.push(format!(" {COL_ID} INTEGER PRIMARY KEY")); for field in &ct.fields { if field.field_type == FieldType::Relation { @@ -418,19 +417,12 @@ fn generate_create_table_for_rebuild( let mut cols = Vec::new(); #[cfg(feature = "db-sqlite")] - cols.push(format!(" {COL_ID} INTEGER PRIMARY KEY AUTOINCREMENT")); + cols.push(format!(" {COL_ID} INTEGER PRIMARY KEY")); #[cfg(feature = "db-postgres")] cols.push(format!(" {COL_ID} BIGSERIAL PRIMARY KEY")); #[cfg(feature = "db-mysql")] cols.push(format!(" {COL_ID} BIGINT AUTO_INCREMENT PRIMARY KEY")); - #[cfg(feature = "db-sqlite")] - cols.push(format!(" {COL_DOCUMENT_ID} TEXT NOT NULL UNIQUE")); - #[cfg(feature = "db-postgres")] - cols.push(format!(" {COL_DOCUMENT_ID} VARCHAR(36) NOT NULL UNIQUE")); - #[cfg(feature = "db-mysql")] - cols.push(format!(" {COL_DOCUMENT_ID} VARCHAR(36) NOT NULL UNIQUE")); - for field in &ct.fields { if field.field_type == FieldType::Relation { match field.relation.as_ref().map(|r| &r.relation_type) { @@ -520,7 +512,7 @@ fn type_compatible(expected: &str, actual: &str) -> bool { /// Get all column names expected by the content type schema (for comparison with DB) #[must_use] pub fn expected_columns(ct: &ContentTypeSchema, protocol_columns: &[ColumnDef]) -> Vec { - let mut cols = vec![COL_ID.to_string(), COL_DOCUMENT_ID.to_string()]; + let mut cols = vec![COL_ID.to_string()]; for field in &ct.fields { if field.field_type == FieldType::Relation { @@ -657,10 +649,10 @@ unique = true let sql = generate_create_table(&ct, &default_protocol_columns()); assert!(sql.contains("CREATE TABLE IF NOT EXISTS tags")); - assert!(sql.contains("document_id TEXT NOT NULL UNIQUE")); assert!(sql.contains("name TEXT NOT NULL")); assert!(sql.contains("created_at TEXT")); assert!(!sql.contains("status")); + assert!(!sql.contains("document_id")); } #[test] @@ -721,7 +713,6 @@ default = false let existing = vec![ "id".into(), - "document_id".into(), "title".into(), "slug".into(), "content".into(), @@ -768,7 +759,6 @@ unique = true let existing = vec![ "id".into(), - "document_id".into(), "name".into(), "slug".into(), "created_at".into(), @@ -799,7 +789,7 @@ required = true ) .unwrap(); - let existing = vec!["id".into(), "document_id".into(), "title".into()]; + let existing = vec!["id".into(), "title".into()]; let stmts = generate_alter_table(&ct, &existing, &soft_delete_protocol_columns()); assert!(stmts.iter().any(|s| s.contains("created_at TEXT"))); @@ -831,7 +821,6 @@ foreign_key = "author_id" let cols = expected_columns(&ct, &default_protocol_columns()); assert!(cols.contains(&"id".to_string())); - assert!(cols.contains(&"document_id".to_string())); assert!(cols.contains(&"title".to_string())); assert!(cols.contains(&"author_id".to_string())); assert!(cols.contains(&"created_by".to_string())); diff --git a/src/content_type/repository.rs b/src/content_type/repository.rs index 635fccba..526579ab 100644 --- a/src/content_type/repository.rs +++ b/src/content_type/repository.rs @@ -31,8 +31,8 @@ pub struct SaveContext { impl SaveContext { pub fn from_auth(auth: &AuthUser) -> Self { Self { - user_id: auth.user_id().map(|s| s.to_string()), - user_int_id: auth.user_int_id(), + user_id: auth.user_id().map(|id| id.to_string()), + user_int_id: auth.user_id(), user_role: auth.is_authenticated().then(|| auth.role().to_string()), tenant_id: auth.tenant_id().map(|s| s.to_string()), } @@ -208,7 +208,7 @@ impl ContentRepository { pub async fn find_by_id( &self, ct: &ContentTypeSchema, - id: &str, + id: i64, tenant_id: Option<&str>, include_private: bool, ) -> Result, AppError> { @@ -216,7 +216,7 @@ impl ContentRepository { let select_cols = columns.join(", "); let tid = self.resolve_tenant(ct, tenant_id); - let mut where_parts = vec![format!("{COL_DOCUMENT_ID} = {}", crate::db::dialect::ph(1))]; + let mut where_parts = vec![format!("{COL_ID} = {}", crate::db::dialect::ph(1))]; let mut idx = 2; if tid.is_some() { where_parts.push(format!("{COL_TENANT_ID} = {}", crate::db::dialect::ph(idx))); @@ -309,6 +309,7 @@ impl ContentRepository { } /// Find by slug + #[allow(dead_code)] pub async fn find_by_slug( &self, ct: &ContentTypeSchema, @@ -370,14 +371,13 @@ impl ContentRepository { let mut tx = self.pool.begin().await?; super::validation::validate_create_tx(&self.pool, ct, &data).await?; - let document_id = crate::utils::id::new_document_id(); + let new_id = crate::utils::id::new_id(); let obj = data .as_object_mut() .ok_or_else(|| AppError::BadRequest("request body must be a JSON object".into()))?; obj.remove(COL_ID); - obj.insert(COL_DOCUMENT_ID.into(), json!(document_id)); let tid = self.resolve_tenant(ct, tenant_id); @@ -386,6 +386,11 @@ impl ContentRepository { let mut values: Vec = Vec::new(); let mut idx = 1; + cols.push(COL_ID.to_string()); + placeholders.push(crate::db::dialect::ph(idx)); + idx += 1; + values.push(new_id.to_string()); + if let Some(ref tid) = tid { cols.push(COL_TENANT_ID.to_string()); placeholders.push(crate::db::dialect::ph(idx)); @@ -454,18 +459,19 @@ impl ContentRepository { } if let Some((fk_col, target_table)) = fk_relation_map.get(key) { - let doc_id = value_to_string(val); - if doc_id.is_empty() { + let target_id = value_to_string(val); + if target_id.is_empty() { cols.push(fk_col.clone()); placeholders.push(crate::db::dialect::ph(idx)); idx += 1; values.push(String::new()); } else { - let int_id = resolve_document_id_to_int_id(&self.pool, target_table, &doc_id) + let parsed_id = crate::utils::id::parse_id(&target_id)?; + let int_id = find_existing_id(&self.pool, target_table, parsed_id) .await? .ok_or_else(|| { AppError::BadRequest(format!( - "relation target '{doc_id}' not found in {target_table}" + "relation target '{target_id}' not found in {target_table}" )) })?; cols.push(fk_col.clone()); @@ -496,17 +502,7 @@ impl ContentRepository { query.execute(&mut *tx).await?; - let source_int_id: i64 = { - let id_sql = format!( - "SELECT {COL_ID} FROM {} WHERE {COL_DOCUMENT_ID} = {}", - ct.table, - crate::db::dialect::ph(1) - ); - sqlx::query_scalar::<_, i64>(&id_sql) - .bind(&document_id) - .fetch_one(&mut *tx) - .await? - }; + let source_int_id = new_id; for (field_name, through_table, target_table, source_col, target_col) in &junction_fields { if !crate::db::dialect::is_safe_identifier(through_table) @@ -521,11 +517,12 @@ impl ContentRepository { let Some(val) = obj.get(field_name) else { continue; }; - let doc_ids = extract_document_ids(val); - if doc_ids.is_empty() { + let ids = extract_ids(val); + if ids.is_empty() { continue; } - let int_ids = resolve_document_ids_batch(&self.pool, target_table, &doc_ids).await?; + let parsed_ids: Vec = ids.iter().filter_map(|s| s.parse().ok()).collect(); + let int_ids = find_existing_ids(&self.pool, target_table, &parsed_ids).await?; for target_int_id in int_ids { let jsql = format!( "INSERT OR IGNORE INTO {through_table} ({source_col}, {target_col}) VALUES ({}, {})", @@ -552,11 +549,12 @@ impl ContentRepository { let Some(val) = obj.get(field_name) else { continue; }; - let doc_ids = extract_document_ids(val); - if doc_ids.is_empty() { + let ids = extract_ids(val); + if ids.is_empty() { continue; } - let int_ids = resolve_document_ids_batch(&self.pool, target_table, &doc_ids).await?; + let parsed_ids: Vec = ids.iter().filter_map(|s| s.parse().ok()).collect(); + let int_ids = find_existing_ids(&self.pool, target_table, &parsed_ids).await?; let usql = format!( "UPDATE {target_table} SET {fk_col} = {} WHERE {COL_ID} = {}", crate::db::dialect::ph(1), @@ -578,12 +576,12 @@ impl ContentRepository { let columns = ct.column_names(None, true); let select_cols = columns.join(", "); let sql = format!( - "SELECT {select_cols} FROM {} WHERE {COL_DOCUMENT_ID} = {}", + "SELECT {select_cols} FROM {} WHERE {COL_ID} = {}", ct.table, crate::db::dialect::ph(1) ); let row = sqlx::query(&sql) - .bind(&document_id) + .bind(new_id.to_string()) .fetch_optional(&self.pool) .await?; @@ -598,7 +596,7 @@ impl ContentRepository { pub async fn update( &self, ct: &ContentTypeSchema, - id: &str, + id: i64, mut data: Value, tenant_id: Option<&str>, _save_ctx: &SaveContext, @@ -626,7 +624,6 @@ impl ContentRepository { .ok_or_else(|| AppError::BadRequest("request body must be a JSON object".into()))?; obj.remove(COL_ID); - obj.remove(COL_DOCUMENT_ID); let tid = self.resolve_tenant(ct, tenant_id); @@ -685,18 +682,7 @@ impl ContentRepository { let decl = ct.declaration(); - let source_int_id: i64 = { - let id_sql = format!( - "SELECT {COL_ID} FROM {} WHERE {COL_DOCUMENT_ID} = {}", - ct.table, - crate::db::dialect::ph(1) - ); - sqlx::query_scalar::<_, i64>(&id_sql) - .bind(id) - .fetch_optional(&mut *tx) - .await? - .ok_or_else(|| AppError::not_found(&format!("{}/{}", ct.singular, id)))? - }; + let source_int_id: i64 = id; for (key, val) in obj.iter() { if ct.get_field(key).is_some() || ct.is_protocol_column(key) { @@ -709,20 +695,20 @@ impl ContentRepository { continue; } if let Some((fk_col, target_table)) = fk_relation_map.get(key) { - let doc_id = value_to_string(val); - if doc_id.is_empty() { + let target_id = value_to_string(val); + if target_id.is_empty() { set_clauses.push(format!("{fk_col} = {}", crate::db::dialect::ph(idx))); idx += 1; values.push(String::new()); } else { - let int_id = - resolve_document_id_to_int_id(&self.pool, target_table, &doc_id) - .await? - .ok_or_else(|| { - AppError::BadRequest(format!( - "relation target '{doc_id}' not found in {target_table}" - )) - })?; + let parsed_id = crate::utils::id::parse_id(&target_id)?; + let int_id = find_existing_id(&self.pool, target_table, parsed_id) + .await? + .ok_or_else(|| { + AppError::BadRequest(format!( + "relation target '{target_id}' not found in {target_table}" + )) + })?; set_clauses.push(format!("{fk_col} = {}", crate::db::dialect::ph(idx))); idx += 1; values.push(int_id.to_string()); @@ -745,15 +731,14 @@ impl ContentRepository { } if !set_clauses.is_empty() { - let mut where_parts = vec![format!( - "{COL_DOCUMENT_ID} = {}", - crate::db::dialect::ph(idx) - )]; + let set_value_count = values.len(); + + let mut where_parts = vec![format!("{COL_ID} = {}", crate::db::dialect::ph(idx))]; idx += 1; - values.push(id.to_string()); if let Some(ref tid) = tid { where_parts.push(format!("{COL_TENANT_ID} = {}", crate::db::dialect::ph(idx))); + idx += 1; values.push(tid.clone()); } @@ -772,7 +757,11 @@ impl ContentRepository { ); let mut query = sqlx::query(&sql); - for v in &values { + for v in &values[..set_value_count] { + query = query.bind(v); + } + query = query.bind(id); + for v in &values[set_value_count..] { query = query.bind(v); } @@ -815,11 +804,12 @@ impl ContentRepository { AppError::Internal(anyhow::Error::from(e).context("junction delete failed")) })?; - let doc_ids = extract_document_ids(val); - if doc_ids.is_empty() { + let ids = extract_ids(val); + if ids.is_empty() { continue; } - let int_ids = resolve_document_ids_batch(&self.pool, target_table, &doc_ids).await?; + let parsed_ids: Vec = ids.iter().filter_map(|s| s.parse().ok()).collect(); + let int_ids = find_existing_ids(&self.pool, target_table, &parsed_ids).await?; for target_int_id in int_ids { let jsql = format!( "INSERT OR IGNORE INTO {through_table} ({source_col}, {target_col}) VALUES ({}, {})", @@ -858,11 +848,12 @@ impl ContentRepository { AppError::Internal(anyhow::Error::from(e).context("one-to-many clear failed")) })?; - let doc_ids = extract_document_ids(val); - if doc_ids.is_empty() { + let ids = extract_ids(val); + if ids.is_empty() { continue; } - let int_ids = resolve_document_ids_batch(&self.pool, target_table, &doc_ids).await?; + let parsed_ids: Vec = ids.iter().filter_map(|s| s.parse().ok()).collect(); + let int_ids = find_existing_ids(&self.pool, target_table, &parsed_ids).await?; let usql = format!( "UPDATE {target_table} SET {fk_col} = {} WHERE {COL_ID} = {}", crate::db::dialect::ph(1), @@ -894,7 +885,7 @@ impl ContentRepository { pub async fn delete( &self, ct: &ContentTypeSchema, - id: &str, + id: i64, tenant_id: Option<&str>, protocol_registry: &crate::protocols::ProtocolRegistry, ct_registry: &crate::content_type::ContentTypeRegistry, @@ -952,12 +943,9 @@ impl ContentRepository { let has_cleanup = !source_junctions.is_empty() || !reverse_junctions.is_empty(); let mut idx = 1; - let mut where_parts = vec![format!( - "{COL_DOCUMENT_ID} = {}", - crate::db::dialect::ph(idx) - )]; + let mut where_parts = vec![format!("{COL_ID} = {}", crate::db::dialect::ph(idx))]; idx += 1; - let mut values = vec![id.to_string()]; + let mut values: Vec = Vec::new(); if let Some(ref tid) = tid { where_parts.push(format!("{COL_TENANT_ID} = {}", crate::db::dialect::ph(idx))); idx += 1; @@ -974,6 +962,7 @@ impl ContentRepository { where_parts.join(" AND ") ); let mut query = sqlx::query_scalar::<_, i64>(&id_sql); + query = query.bind(id); for v in &values { query = query.bind(v); } @@ -1040,6 +1029,7 @@ impl ContentRepository { ); let mut query = sqlx::query(&sql); query = query.bind(now); + query = query.bind(id); for v in &values { query = query.bind(v); } @@ -1053,6 +1043,7 @@ impl ContentRepository { where_parts.join(" AND ") ); let mut query = sqlx::query(&sql); + query = query.bind(id); for v in &values { query = query.bind(v); } @@ -1080,6 +1071,7 @@ impl ContentRepository { ); let mut query = sqlx::query(&sql); query = query.bind(now); + query = query.bind(id); for v in &values { query = query.bind(v); } @@ -1094,6 +1086,7 @@ impl ContentRepository { where_parts.join(" AND ") ); let mut query = sqlx::query(&sql); + query = query.bind(id); for v in &values { query = query.bind(v); } @@ -1115,7 +1108,7 @@ impl ContentRepository { pub async fn soft_delete( &self, ct: &ContentTypeSchema, - id: &str, + id: i64, deleted_at: &str, deleted_by: Option, tenant_id: Option<&str>, @@ -1139,10 +1132,7 @@ impl ContentRepository { idx += 1; } - let mut where_parts = vec![format!( - "{COL_DOCUMENT_ID} = {}", - crate::db::dialect::ph(idx) - )]; + let mut where_parts = vec![format!("{COL_ID} = {}", crate::db::dialect::ph(idx))]; idx += 1; if tid.is_some() { @@ -1368,7 +1358,6 @@ pub fn build_column_names( ) -> Vec { let mut cols = Vec::new(); cols.push(COL_ID.into()); - cols.push(COL_DOCUMENT_ID.into()); for field in &ct.fields { if !include_private && field.private { @@ -1427,6 +1416,11 @@ pub(crate) fn row_to_value(row: &DbRow, columns: &[String]) -> Value { /// Note: bool is placed after i64 because SQLite does not distinguish bool from int; /// non-0/1 integers would be misidentified as true by bool. fn cell_to_json(row: &DbRow, col: &str) -> Value { + if col == COL_ID + && let Ok(Some(v)) = row.try_get::, _>(col) + { + return json!(v.to_string()); + } if let Ok(Some(v)) = row.try_get::, _>(col) { return json!(v); } @@ -1512,7 +1506,7 @@ fn value_to_string(v: &Value) -> String { } } -fn extract_document_ids(val: &Value) -> Vec { +fn extract_ids(val: &Value) -> Vec { match val { Value::String(s) if !s.is_empty() => vec![s.clone()], Value::Array(arr) => arr @@ -1524,36 +1518,33 @@ fn extract_document_ids(val: &Value) -> Vec { } } -pub(crate) async fn resolve_document_id_to_int_id( +pub(crate) async fn find_existing_id( pool: &Pool, target_table: &str, - document_id: &str, + id: i64, ) -> Result, AppError> { - if document_id.is_empty() { - return Ok(None); - } if !crate::db::dialect::is_safe_identifier(target_table) { return Err(AppError::BadRequest(format!( "invalid target table: {target_table}" ))); } let sql = format!( - "SELECT {COL_ID} FROM {target_table} WHERE {COL_DOCUMENT_ID} = {}", + "SELECT {COL_ID} FROM {target_table} WHERE {COL_ID} = {}", crate::db::dialect::ph(1) ); let result = sqlx::query_scalar::<_, i64>(&sql) - .bind(document_id) + .bind(id) .fetch_optional(pool) .await?; Ok(result) } -async fn resolve_document_ids_batch( +async fn find_existing_ids( pool: &Pool, target_table: &str, - document_ids: &[String], + ids: &[i64], ) -> Result, AppError> { - if document_ids.is_empty() { + if ids.is_empty() { return Ok(Vec::new()); } if !crate::db::dialect::is_safe_identifier(target_table) { @@ -1561,33 +1552,30 @@ async fn resolve_document_ids_batch( "invalid target table: {target_table}" ))); } - let placeholders: Vec = (1..=document_ids.len()) - .map(crate::db::dialect::ph) - .collect(); + let placeholders: Vec = (1..=ids.len()).map(crate::db::dialect::ph).collect(); let sql = format!( - "SELECT {COL_ID}, {COL_DOCUMENT_ID} FROM {target_table} WHERE {COL_DOCUMENT_ID} IN ({})", + "SELECT {COL_ID} FROM {target_table} WHERE {COL_ID} IN ({})", placeholders.join(", ") ); let mut q = sqlx::query(&sql); - for id in document_ids { - q = q.bind(id); + for id in ids { + q = q.bind(*id); } let rows = q.fetch_all(pool).await?; - let mut lookup: std::collections::HashMap = std::collections::HashMap::new(); + let mut found: std::collections::HashSet = std::collections::HashSet::new(); for row in &rows { let int_id: i64 = row.try_get(COL_ID).unwrap_or(0); - let doc_id: String = row.try_get(COL_DOCUMENT_ID).unwrap_or_default(); - if int_id > 0 && !doc_id.is_empty() { - lookup.insert(doc_id, int_id); + if int_id > 0 { + found.insert(int_id); } } let mut result = Vec::new(); - for doc_id in document_ids { - let int_id = lookup.remove(doc_id).ok_or_else(|| { - AppError::BadRequest(format!( - "relation target document_id '{doc_id}' not found in {target_table}" - )) - })?; + for &int_id in ids { + if !found.contains(&int_id) { + return Err(AppError::BadRequest(format!( + "relation target id '{int_id}' not found in {target_table}" + ))); + } result.push(int_id); } Ok(result) @@ -1674,7 +1662,6 @@ unique = true let cols = build_column_names(&ct, None, false); assert!(cols.contains(&"id".to_string())); - assert!(cols.contains(&"document_id".to_string())); assert!(cols.contains(&"name".to_string())); assert!(cols.contains(&"slug".to_string())); assert!(cols.contains(&"created_at".to_string())); @@ -1745,32 +1732,32 @@ type = "text" } #[test] - fn extract_document_ids_string() { - let ids = extract_document_ids(&json!("abc-123")); + fn extract_ids_string() { + let ids = extract_ids(&json!("abc-123")); assert_eq!(ids, vec!["abc-123"]); } #[test] - fn extract_document_ids_array() { - let ids = extract_document_ids(&json!(["a", "b", "c"])); + fn extract_ids_array() { + let ids = extract_ids(&json!(["a", "b", "c"])); assert_eq!(ids, vec!["a", "b", "c"]); } #[test] - fn extract_document_ids_empty_string() { - let ids = extract_document_ids(&json!("")); + fn extract_ids_empty_string() { + let ids = extract_ids(&json!("")); assert!(ids.is_empty()); } #[test] - fn extract_document_ids_non_string() { - let ids = extract_document_ids(&json!(42)); + fn extract_ids_non_string() { + let ids = extract_ids(&json!(42)); assert!(ids.is_empty()); } #[test] - fn extract_document_ids_array_filters_empty() { - let ids = extract_document_ids(&json!(["a", "", "c"])); + fn extract_ids_array_filters_empty() { + let ids = extract_ids(&json!(["a", "", "c"])); assert_eq!(ids, vec!["a", "c"]); } @@ -1919,13 +1906,12 @@ type = "integer" #[test] fn save_context_from_auth() { let auth = crate::middleware::auth::AuthUser::from_parts( - Some("u1".to_string()), Some(42), crate::models::user::UserRole::Admin, Some("t1".to_string()), ); let ctx = SaveContext::from_auth(&auth); - assert_eq!(ctx.user_id, Some("u1".to_string())); + assert_eq!(ctx.user_id, Some("42".to_string())); assert_eq!(ctx.user_int_id, Some(42)); assert_eq!( ctx.user_role, diff --git a/src/content_type/resolver.rs b/src/content_type/resolver.rs index 043dba09..e8ddd60d 100644 --- a/src/content_type/resolver.rs +++ b/src/content_type/resolver.rs @@ -13,6 +13,11 @@ use sqlx::Row; use super::schema::{ContentTypeSchema, FieldType, RelationType}; use crate::constants::COL_ID; + +fn extract_id_i64(v: &Value) -> Option { + v.as_i64() + .or_else(|| v.as_str().and_then(|s| s.parse().ok())) +} use crate::db::Pool; use crate::errors::app_error::AppError; @@ -135,7 +140,7 @@ async fn resolve_many_to_one_batch( let mut lookup: std::collections::HashMap = std::collections::HashMap::new(); for row in &rows { let val = super::repository::row_to_value(row, &columns); - if let Some(id) = val.get(COL_ID).and_then(|v| v.as_i64()) { + if let Some(id) = val.get(COL_ID).and_then(extract_id_i64) { lookup.insert(id, val); } } @@ -183,7 +188,7 @@ async fn resolve_one_to_many_batch( let item_ids: Vec = items .iter() - .filter_map(|item| item.get(COL_ID).and_then(|v| v.as_i64())) + .filter_map(|item| item.get(COL_ID).and_then(extract_id_i64)) .filter(|&id| id > 0) .collect(); @@ -225,7 +230,7 @@ async fn resolve_one_to_many_batch( } for item in items.iter_mut() { - let Some(item_id) = item.get(COL_ID).and_then(|v| v.as_i64()) else { + let Some(item_id) = item.get(COL_ID).and_then(extract_id_i64) else { continue; }; let targets = lookup.get(&item_id).cloned().unwrap_or_default(); @@ -266,7 +271,7 @@ async fn resolve_many_to_many_batch( let item_ids: Vec = items .iter() - .filter_map(|item| item.get(COL_ID).and_then(|v| v.as_i64())) + .filter_map(|item| item.get(COL_ID).and_then(extract_id_i64)) .filter(|&id| id > 0) .collect(); @@ -310,7 +315,7 @@ async fn resolve_many_to_many_batch( } for item in items.iter_mut() { - let Some(item_id) = item.get(COL_ID).and_then(|v| v.as_i64()) else { + let Some(item_id) = item.get(COL_ID).and_then(extract_id_i64) else { continue; }; let targets = lookup.get(&item_id).cloned().unwrap_or_default(); @@ -402,21 +407,21 @@ through = "ct_resolve_posts_tags" let pool = crate::db::Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_users (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT, slug TEXT, title TEXT)", + "CREATE TABLE ct_resolve_users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, slug TEXT, title TEXT)", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_tags (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT, slug TEXT, title TEXT)", + "CREATE TABLE ct_resolve_tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, slug TEXT, title TEXT)", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, author_id INTEGER, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, created_by INTEGER, updated_by INTEGER)", + "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author_id INTEGER, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, created_by INTEGER, updated_by INTEGER)", ) .execute(&pool) .await @@ -429,20 +434,22 @@ through = "ct_resolve_posts_tags" .await .unwrap(); - sqlx::query("INSERT INTO ct_resolve_users (id, document_id, name, slug, title) VALUES (1, 'u1', 'Alice', 'alice', '')") - .execute(&pool) - .await - .unwrap(); - sqlx::query( - "INSERT INTO ct_resolve_tags (id, document_id, name, slug, title) VALUES (1, 't1', 'Rust', 'rust', '')", + "INSERT INTO ct_resolve_users (id, name, slug, title) VALUES (1, 'Alice', 'alice', '')", ) .execute(&pool) .await .unwrap(); sqlx::query( - "INSERT INTO ct_resolve_tags (id, document_id, name, slug, title) VALUES (2, 't2', 'Web', 'web', '')", + "INSERT INTO ct_resolve_tags (id, name, slug, title) VALUES (1, 'Rust', 'rust', '')", + ) + .execute(&pool) + .await + .unwrap(); + + sqlx::query( + "INSERT INTO ct_resolve_tags (id, name, slug, title) VALUES (2, 'Web', 'web', '')", ) .execute(&pool) .await @@ -458,7 +465,7 @@ through = "ct_resolve_posts_tags" let mut items = vec![serde_json::json!({ "id": 1, - "document_id": "p1", + "title": "Hello", "author_id": 1, "created_at": "2024-01-01T00:00:00Z", @@ -479,7 +486,7 @@ through = "ct_resolve_posts_tags" let ct = make_ct_with_relations(); sqlx::query( - "INSERT INTO ct_resolve_posts (id, document_id, title, author_id, created_at, updated_at) VALUES (1, 'p1', 'Hello', 1, '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z')", + "INSERT INTO ct_resolve_posts (id, title, author_id, created_at, updated_at) VALUES (1, 'Hello', 1, '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z')", ) .execute(&pool) .await @@ -500,7 +507,7 @@ through = "ct_resolve_posts_tags" let mut items = vec![serde_json::json!({ "id": 1, - "document_id": "p1", + "title": "Hello", "author_id": 1, "created_at": "2024-01-01T00:00:00Z", @@ -525,7 +532,7 @@ through = "ct_resolve_posts_tags" let mut items = vec![serde_json::json!({ "id": 1, - "document_id": "p1", + "title": "Hello", "author_id": 1, "created_at": "2024-01-01T00:00:00Z", @@ -555,29 +562,29 @@ through = "ct_resolve_posts_tags" let pool = crate::db::Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, text TEXT, post_id INTEGER)", + "CREATE TABLE ct_resolve_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, post_id INTEGER)", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, author_id INTEGER, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)", + "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author_id INTEGER, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)", ) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_resolve_posts (id, document_id, title, author_id, created_at, updated_at) VALUES (1, 'p1', 'Hello', 0, '2024-01-01', '2024-01-01')") + sqlx::query("INSERT INTO ct_resolve_posts (id, title, author_id, created_at, updated_at) VALUES (1, 'Hello', 0, '2024-01-01', '2024-01-01')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_resolve_comments (id, document_id, text, post_id) VALUES (1, 'c1', 'Nice', 1)") + sqlx::query("INSERT INTO ct_resolve_comments (id, text, post_id) VALUES (1, 'Nice', 1)") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_resolve_comments (id, document_id, text, post_id) VALUES (2, 'c2', 'Great', 1)") + sqlx::query("INSERT INTO ct_resolve_comments (id, text, post_id) VALUES (2, 'Great', 1)") .execute(&pool) .await .unwrap(); @@ -604,7 +611,7 @@ foreign_key = "post_id" let mut items = vec![serde_json::json!({ "id": 1, - "document_id": "p1", + "title": "Hello", "created_at": "2024-01-01", "updated_at": "2024-01-01" @@ -625,15 +632,13 @@ foreign_key = "post_id" async fn resolve_m2o_with_zero_fk_skipped() { let pool = crate::db::Pool::connect(":memory:").await.unwrap(); - sqlx::query( - "CREATE TABLE ct_resolve_users (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT)", - ) - .execute(&pool) - .await - .unwrap(); + sqlx::query("CREATE TABLE ct_resolve_users (id INTEGER PRIMARY KEY, name TEXT)") + .execute(&pool) + .await + .unwrap(); sqlx::query( - "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, author_id INTEGER)", + "CREATE TABLE ct_resolve_posts (id INTEGER PRIMARY KEY, title TEXT, author_id INTEGER)", ) .execute(&pool) .await @@ -661,7 +666,7 @@ foreign_key = "author_id" let mut items = vec![serde_json::json!({ "id": 1, - "document_id": "p1", + "title": "NoAuthor", "author_id": 0 })]; diff --git a/src/content_type/rule_engine.rs b/src/content_type/rule_engine.rs index 2c462456..543ef850 100644 --- a/src/content_type/rule_engine.rs +++ b/src/content_type/rule_engine.rs @@ -50,7 +50,7 @@ impl RuleContext { /// Build context from request authentication info pub fn from_auth(auth: &crate::middleware::auth::AuthUser) -> Self { Self { - auth_user_id: auth.user_id().map(|s| s.to_string()), + auth_user_id: auth.user_id().map(|id| id.to_string()), auth_role: if auth.is_authenticated() { Some(auth.role().to_string()) } else { @@ -620,7 +620,7 @@ pub fn compile_rule_sql( if auth.is_authenticated() { if expr_has_auth_id(&rule.expr) { let idx = offset + params.len() + 1; - params.push(auth.user_id().unwrap_or("").to_string()); + params.push(auth.user_id().map_or_else(String::new, |id| id.to_string())); sql = sql.replace("__AUTH_ID__", &crate::db::dialect::ph(idx)); } if expr_has_auth_role(&rule.expr) { @@ -1122,7 +1122,7 @@ mod tests { let rule = Rule::parse(r#"status = "published""#, &cfg).unwrap(); let record = json!({"status": "published"}); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1135,7 +1135,7 @@ mod tests { let rule = Rule::parse(r#"status = "published""#, &cfg).unwrap(); let record = json!({"status": "draft"}); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1175,7 +1175,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse(r#"status = "published" || status = "draft""#, &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1194,7 +1194,7 @@ mod tests { fn eval_comparison_operators() { let cfg = default_config(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1218,7 +1218,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse(r#"title ~ "%rust%""#, &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1232,7 +1232,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse(r#"title !~ "%spam%""#, &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1246,7 +1246,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse("title:isset", &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1261,7 +1261,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse("tags:length > 0", &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1345,7 +1345,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse("status = null", &cfg).unwrap(); let ctx = RuleContext::from_auth(&crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", )); @@ -1361,7 +1361,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse(r#"status = "published""#, &cfg).unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ); @@ -1375,13 +1375,13 @@ mod tests { let cfg = default_config(); let rule = Rule::parse("author_id = @request.auth.id", &cfg).unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "user123", + 123, crate::models::user::UserRole::Reader, "", ); let (sql, params) = compile_rule_sql(&rule, 0, &auth, &cfg).unwrap(); assert!(!sql.contains("__AUTH_ID__")); - assert!(params.contains(&"user123".to_string())); + assert!(params.contains(&"123".to_string())); } #[test] @@ -1389,7 +1389,7 @@ mod tests { let cfg = default_config(); let rule = Rule::parse("author_id = @request.auth.id", &cfg).unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ); @@ -1405,7 +1405,7 @@ mod tests { ) .unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "abc", + 999, crate::models::user::UserRole::Reader, "", ); @@ -1413,7 +1413,7 @@ mod tests { assert!(sql.contains("OR")); assert!(!sql.contains("__AUTH_ID__")); assert!(params.contains(&"published".to_string())); - assert!(params.contains(&"abc".to_string())); + assert!(params.contains(&"999".to_string())); } // ── Configuration tests ────────────────────────────────────── diff --git a/src/content_type/schema.rs b/src/content_type/schema.rs index 458008d0..94a53178 100644 --- a/src/content_type/schema.rs +++ b/src/content_type/schema.rs @@ -1483,7 +1483,6 @@ type = "text" #[test] fn check_api_access_none_rejects() { let auth = crate::middleware::auth::AuthUser::from_parts( - Some("u1".into()), Some(1), crate::models::user::UserRole::Admin, None, @@ -1494,7 +1493,6 @@ type = "text" #[test] fn check_api_access_public_allows_all() { let anon = crate::middleware::auth::AuthUser::from_parts( - None, None, crate::models::user::UserRole::Reader, None, @@ -1505,14 +1503,12 @@ type = "text" #[test] fn check_api_access_member_requires_auth() { let anon = crate::middleware::auth::AuthUser::from_parts( - None, None, crate::models::user::UserRole::Reader, None, ); assert!(check_api_access(ApiAccess::Member, &anon).is_err()); let user = crate::middleware::auth::AuthUser::from_parts( - Some("u1".into()), Some(1), crate::models::user::UserRole::Reader, None, @@ -1523,14 +1519,12 @@ type = "text" #[test] fn check_api_access_admin_requires_admin_role() { let anon = crate::middleware::auth::AuthUser::from_parts( - None, None, crate::models::user::UserRole::Reader, None, ); assert!(check_api_access(ApiAccess::Admin, &anon).is_err()); let user = crate::middleware::auth::AuthUser::from_parts( - Some("u1".into()), Some(1), crate::models::user::UserRole::Reader, None, @@ -1538,7 +1532,6 @@ type = "text" let err = check_api_access(ApiAccess::Admin, &user).unwrap_err(); assert!(matches!(err, AppError::Forbidden)); let admin = crate::middleware::auth::AuthUser::from_parts( - Some("u1".into()), Some(1), crate::models::user::UserRole::Admin, None, diff --git a/src/content_type/validation.rs b/src/content_type/validation.rs index d9ebbcd7..2eb15795 100644 --- a/src/content_type/validation.rs +++ b/src/content_type/validation.rs @@ -14,7 +14,7 @@ use serde_json::Value; use sqlx::Row; use super::schema::{ContentTypeSchema, FieldType, RelationType}; -use crate::constants::COL_DOCUMENT_ID; +use crate::constants::COL_ID; use crate::db::Pool; use crate::errors::app_error::AppError; @@ -71,7 +71,7 @@ async fn do_validate_create( RelationType::ManyToOne | RelationType::OneToOne | RelationType::OneWay => { if !val.is_string() && !val.is_null() { errors.push(format!( - "field '{}': expected string (document_id) or null", + "field '{}': expected string (id) or null", field.name )); } @@ -79,7 +79,7 @@ async fn do_validate_create( RelationType::ManyToMany | RelationType::ManyWay => { if !val.is_array() && !val.is_null() { errors.push(format!( - "field '{}': expected array of document_ids or null", + "field '{}': expected array of ids or null", field.name )); } @@ -187,7 +187,7 @@ async fn do_validate_create( pub async fn validate_update( pool: &Pool, ct: &ContentTypeSchema, - id: &str, + id: i64, data: &Value, ) -> Result<(), AppError> { do_validate_update(pool, ct, id, data).await @@ -197,7 +197,7 @@ pub async fn validate_update( pub async fn validate_update_tx( pool: &Pool, ct: &ContentTypeSchema, - id: &str, + id: i64, data: &Value, ) -> Result<(), AppError> { do_validate_update(pool, ct, id, data).await @@ -206,7 +206,7 @@ pub async fn validate_update_tx( async fn do_validate_update( pool: &Pool, ct: &ContentTypeSchema, - id: &str, + id: i64, data: &Value, ) -> Result<(), AppError> { let obj = data @@ -240,7 +240,7 @@ async fn do_validate_update( RelationType::ManyToOne | RelationType::OneToOne | RelationType::OneWay => { if !val.is_string() && !val.is_null() { errors.push(format!( - "field '{}': expected string (document_id) or null", + "field '{}': expected string (id) or null", field.name )); } @@ -248,7 +248,7 @@ async fn do_validate_update( RelationType::ManyToMany | RelationType::ManyWay => { if !val.is_array() && !val.is_null() { errors.push(format!( - "field '{}': expected array of document_ids or null", + "field '{}': expected array of ids or null", field.name )); } @@ -356,7 +356,7 @@ async fn check_unique_fields( pool: &Pool, ct: &ContentTypeSchema, obj: &serde_json::Map, - exclude_id: Option<&str>, + exclude_id: Option, errors: &mut Vec, ) -> Result<(), AppError> { let mut sql_builder = String::new(); @@ -375,7 +375,7 @@ async fn check_unique_fields( sql_builder.clear(); if exclude_id.is_some() { sql_builder = format!( - "SELECT COUNT(*) as cnt FROM {} WHERE {} = {} AND {COL_DOCUMENT_ID} != {}", + "SELECT COUNT(*) as cnt FROM {} WHERE {} = {} AND {COL_ID} != {}", ct.table, field.name, crate::db::dialect::ph(1), @@ -676,9 +676,9 @@ immutable = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); - let result = validate_update(&pool, &ct, &id, &json!({"secret": "new"})).await; + let result = validate_update(&pool, &ct, id, &json!({"secret": "new"})).await; assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!( @@ -704,10 +704,10 @@ immutable = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); let result = - validate_update(&pool, &ct, &id, &json!({"name": "Updated", "code": "XYZ"})).await; + validate_update(&pool, &ct, id, &json!({"name": "Updated", "code": "XYZ"})).await; assert!(result.is_ok(), "updating same unique value should be ok"); } @@ -818,7 +818,7 @@ immutable = true let repo = crate::content_type::repository::ContentRepository::new(pool.clone()); repo.migrate(&ct, &test_protocol_registry()).await.unwrap(); - let result = validate_update(&pool, &ct, "some-id", &json!(42)).await; + let result = validate_update(&pool, &ct, 0, &json!(42)).await; assert!(result.is_err()); } diff --git a/src/db/tenant.rs b/src/db/tenant.rs index 25d04a35..42f701ea 100644 --- a/src/db/tenant.rs +++ b/src/db/tenant.rs @@ -187,8 +187,8 @@ pub fn tenant_filter_aliased(alias: &str, tenant_id: Option<&str>) -> String { /// # Example /// /// ```ignore -/// let sql = insert_sql("tags", &["document_id", "name", "slug"], tenant_id); -/// let mut q = sqlx::query(&sql).bind(&doc_id).bind(name).bind(slug); +/// let sql = insert_sql("tags", &["id", "name", "slug"], tenant_id); +/// let mut q = sqlx::query(&sql).bind(&id).bind(name).bind(slug); /// bind_tenant!(q, tenant_id); /// q.execute(pool).await?; /// ``` @@ -336,10 +336,12 @@ mod tests { #[tokio::test] async fn prepare_select_injects_when_has_column() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - sqlx::query("CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, tenant_id TEXT)") - .execute(&pool) - .await - .unwrap(); + sqlx::query( + "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, tenant_id TEXT)", + ) + .execute(&pool) + .await + .unwrap(); invalidate_cache().await; let tp = TenantPool::new(pool, "t1"); @@ -353,7 +355,7 @@ mod tests { #[tokio::test] async fn prepare_select_skips_when_no_column() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - sqlx::query("CREATE TABLE logs (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, msg TEXT)") + sqlx::query("CREATE TABLE logs (id INTEGER PRIMARY KEY AUTOINCREMENT, msg TEXT)") .execute(&pool) .await .unwrap(); @@ -370,14 +372,16 @@ mod tests { #[tokio::test] async fn prepare_insert_injects_column() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - sqlx::query("CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT, tenant_id TEXT)") - .execute(&pool) - .await - .unwrap(); + sqlx::query( + "CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, tenant_id TEXT)", + ) + .execute(&pool) + .await + .unwrap(); invalidate_cache().await; let tp = TenantPool::new(pool, "t1"); - let (sql, bind) = tp.prepare_insert("items", "document_id, name", 2).await; + let (sql, bind) = tp.prepare_insert("items", "id, name", 2).await; assert!(bind); assert!(sql.contains("tenant_id")); assert!(sql.contains("?, ?")); // 2 user + 1 tenant @@ -387,40 +391,32 @@ mod tests { async fn end_to_end_select_filters_by_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query( - "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", - ) - .execute(&pool) - .await - .unwrap(); - sqlx::query( - "INSERT INTO posts (document_id, title, tenant_id) VALUES ('doc1', 'Hello', 't1')", - ) - .execute(&pool) - .await - .unwrap(); - sqlx::query( - "INSERT INTO posts (document_id, title, tenant_id) VALUES ('doc2', 'World', 't2')", + "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); + sqlx::query("INSERT INTO posts (id, title, tenant_id) VALUES (1, 'Hello', 't1')") + .execute(&pool) + .await + .unwrap(); + sqlx::query("INSERT INTO posts (id, title, tenant_id) VALUES (2, 'World', 't2')") + .execute(&pool) + .await + .unwrap(); invalidate_cache().await; #[derive(sqlx::FromRow)] #[allow(dead_code)] struct Post { - document_id: String, title: String, } let tp = TenantPool::new(pool, "t1"); let (sql, bind) = tp - .prepare_select( - "posts", - "SELECT document_id, title FROM posts WHERE document_id = ?", - ) + .prepare_select("posts", "SELECT title FROM posts WHERE id = ?") .await; - let mut q = sqlx::query_as::<_, Post>(&sql).bind("doc1"); + let mut q = sqlx::query_as::<_, Post>(&sql).bind(1i64); if bind { q = q.bind(tp.tenant_id()); } @@ -428,12 +424,9 @@ mod tests { assert_eq!(p.title, "Hello"); let (sql, bind) = tp - .prepare_select( - "posts", - "SELECT document_id, title FROM posts WHERE document_id = ?", - ) + .prepare_select("posts", "SELECT title FROM posts WHERE id = ?") .await; - let mut q = sqlx::query_as::<_, Post>(&sql).bind("doc2"); + let mut q = sqlx::query_as::<_, Post>(&sql).bind(2i64); if bind { q = q.bind(tp.tenant_id()); } @@ -444,7 +437,7 @@ mod tests { async fn end_to_end_insert_auto_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query( - "CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await @@ -452,18 +445,17 @@ mod tests { invalidate_cache().await; let tp = TenantPool::new(pool.clone(), "t1"); - let (sql, bind) = tp.prepare_insert("items", "document_id, name", 2).await; - let mut q = sqlx::query(&sql).bind("i1").bind("Test"); + let (sql, bind) = tp.prepare_insert("items", "id, name", 2).await; + let mut q = sqlx::query(&sql).bind(1i64).bind("Test"); if bind { q = q.bind(tp.tenant_id()); } q.execute(tp.pool()).await.unwrap(); - let row: (String, String, String) = - sqlx::query_as("SELECT document_id, name, tenant_id FROM items") - .fetch_one(&pool) - .await - .unwrap(); + let row: (i64, String, String) = sqlx::query_as("SELECT id, name, tenant_id FROM items") + .fetch_one(&pool) + .await + .unwrap(); assert_eq!(row.2, "t1"); } @@ -471,16 +463,16 @@ mod tests { async fn end_to_end_delete_respects_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query( - "CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, name TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO items (document_id, name, tenant_id) VALUES ('d1', 'A', 't1')") + sqlx::query("INSERT INTO items (id, name, tenant_id) VALUES (1, 'A', 't1')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO items (document_id, name, tenant_id) VALUES ('d2', 'B', 't2')") + sqlx::query("INSERT INTO items (id, name, tenant_id) VALUES (2, 'B', 't2')") .execute(&pool) .await .unwrap(); @@ -489,9 +481,9 @@ mod tests { let tp = TenantPool::new(pool.clone(), "t1"); let (sql, bind) = tp - .prepare_modify("items", "DELETE FROM items WHERE document_id = ?") + .prepare_modify("items", "DELETE FROM items WHERE id = ?") .await; - let mut q = sqlx::query(&sql).bind("d2"); + let mut q = sqlx::query(&sql).bind(2i64); if bind { q = q.bind(tp.tenant_id()); } @@ -503,9 +495,9 @@ mod tests { assert_eq!(count.0, 2); let (sql, bind) = tp - .prepare_modify("items", "DELETE FROM items WHERE document_id = ?") + .prepare_modify("items", "DELETE FROM items WHERE id = ?") .await; - let mut q = sqlx::query(&sql).bind("d1"); + let mut q = sqlx::query(&sql).bind(1i64); if bind { q = q.bind(tp.tenant_id()); } diff --git a/src/dto.rs b/src/dto.rs index a9deb4f3..9c62de3e 100644 --- a/src/dto.rs +++ b/src/dto.rs @@ -6,9 +6,12 @@ pub mod currencies; pub mod ecommerce; pub mod media; pub mod order; +pub mod page; pub mod payment; pub mod post; +pub mod reusable_block; pub mod tag; +pub mod tenant; pub mod user; pub mod wallet; @@ -20,9 +23,12 @@ pub use currencies::*; pub use ecommerce::*; pub use media::*; pub use order::*; +pub use page::*; pub use payment::*; pub use post::*; +pub use reusable_block::*; pub use tag::*; +pub use tenant::*; pub use user::*; pub use wallet::*; diff --git a/src/dto/cart.rs b/src/dto/cart.rs index b7220ab4..c2d4c912 100644 --- a/src/dto/cart.rs +++ b/src/dto/cart.rs @@ -24,7 +24,6 @@ pub struct UpdateCartItemRequest { #[derive(Debug, Serialize, ToSchema)] pub struct CartItemResponse { pub id: String, - pub product_id: String, #[cfg_attr(feature = "export-types", ts(type = "number"))] pub quantity: i64, pub attributes: Option, diff --git a/src/dto/category.rs b/src/dto/category.rs index 58deda46..c4a7bf26 100644 --- a/src/dto/category.rs +++ b/src/dto/category.rs @@ -6,6 +6,35 @@ use validator::Validate; use super::validate_optional_uuid; +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, ToSchema)] +pub struct CategoryResponse { + pub id: String, + pub name: String, + pub slug: String, + pub description: Option, + #[cfg_attr(feature = "export-types", ts(type = "number"))] + pub sort_order: i64, + pub cover_image: Option, + pub created_at: String, + pub updated_at: String, +} + +impl CategoryResponse { + pub fn from_category(cat: crate::models::category::Category) -> Self { + Self { + id: cat.id.to_string(), + name: cat.name, + slug: cat.slug, + description: cat.description, + sort_order: cat.sort_order, + cover_image: cat.cover_image, + created_at: cat.created_at.to_rfc3339(), + updated_at: cat.updated_at.to_rfc3339(), + } + } +} + #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)] pub struct CreateCategoryRequest { diff --git a/src/dto/currencies.rs b/src/dto/currencies.rs index 61176e81..1d024135 100644 --- a/src/dto/currencies.rs +++ b/src/dto/currencies.rs @@ -19,7 +19,7 @@ pub struct CurrencyResponse { impl From for CurrencyResponse { fn from(c: Currency) -> Self { Self { - id: c.document_id, + id: c.id.to_string(), code: c.code, name: c.name, decimals: c.decimals, diff --git a/src/dto/ecommerce.rs b/src/dto/ecommerce.rs index f47fda31..3afe6f8b 100644 --- a/src/dto/ecommerce.rs +++ b/src/dto/ecommerce.rs @@ -40,7 +40,6 @@ pub struct UpdateProductVariantRequest { #[derive(Debug, Serialize, ToSchema)] pub struct ProductVariantResponse { pub id: String, - pub product_id: String, pub sku: Option, pub title: String, #[cfg_attr(feature = "export-types", ts(type = "number"))] @@ -59,8 +58,7 @@ pub struct ProductVariantResponse { impl From for ProductVariantResponse { fn from(v: crate::models::product_variant::ProductVariant) -> Self { Self { - id: v.document_id, - product_id: v.product_id.to_string(), + id: v.id.to_string(), sku: v.sku, title: v.title, price: v.price, @@ -146,7 +144,7 @@ pub struct UserAddressResponse { impl From for UserAddressResponse { fn from(a: crate::models::user_address::UserAddress) -> Self { Self { - id: a.document_id, + id: a.id.to_string(), label: a.label, recipient_name: a.recipient_name, phone: a.phone, diff --git a/src/dto/media.rs b/src/dto/media.rs index d17f32ef..7b44265c 100644 --- a/src/dto/media.rs +++ b/src/dto/media.rs @@ -11,7 +11,6 @@ use crate::utils::tz::Timestamp; #[derive(Debug, Serialize, ToSchema)] pub struct MediaResponse { pub id: String, - pub user_id: String, pub filename: String, pub url: String, pub mimetype: String, @@ -32,8 +31,7 @@ pub struct MediaResponse { #[must_use] pub fn media_to_response(media: &Media, base_url: &str) -> MediaResponse { MediaResponse { - id: media.document_id.clone(), - user_id: media.user_id.to_string(), + id: media.id.to_string(), filename: media.filename.clone(), url: format!("{}/uploads/{}", base_url, media.filepath), mimetype: media.mimetype.clone(), @@ -53,8 +51,7 @@ pub fn media_to_response(media: &Media, base_url: &str) -> MediaResponse { #[must_use] pub fn media_to_response_with_url(media: &Media, url: &str) -> MediaResponse { MediaResponse { - id: media.document_id.clone(), - user_id: media.user_id.to_string(), + id: media.id.to_string(), filename: media.filename.clone(), url: url.to_string(), mimetype: media.mimetype.clone(), diff --git a/src/dto/order.rs b/src/dto/order.rs index 4d98ca3b..74dab650 100644 --- a/src/dto/order.rs +++ b/src/dto/order.rs @@ -108,7 +108,6 @@ pub struct ShipOrderRequest { #[derive(Debug, Serialize, ToSchema)] pub struct ProductResponse { pub id: String, - pub category_id: Option, pub title: String, pub description: Option, pub cover_url: Option, @@ -153,8 +152,7 @@ pub struct ProductResponse { impl From for ProductResponse { fn from(p: crate::models::product::Product) -> Self { Self { - id: p.document_id, - category_id: p.category_id.map(|_| String::new()), + id: p.id.to_string(), title: p.title, description: p.description, cover_url: p.cover_url, @@ -191,8 +189,6 @@ impl From for ProductResponse { #[derive(Debug, Serialize, ToSchema)] pub struct OrderItemResponse { pub id: String, - pub order_id: String, - pub product_id: Option, pub title: String, pub description: Option, #[cfg_attr(feature = "export-types", ts(type = "number"))] @@ -209,9 +205,7 @@ pub struct OrderItemResponse { impl From for OrderItemResponse { fn from(i: crate::models::order_item::OrderItem) -> Self { Self { - id: i.document_id, - order_id: i.order_id.to_string(), - product_id: i.product_id.map(|id| id.to_string()), + id: i.id.to_string(), title: i.title, description: i.description, unit_price: i.unit_price, @@ -228,7 +222,6 @@ impl From for OrderItemResponse { #[derive(Debug, Serialize, ToSchema)] pub struct OrderResponse { pub id: String, - pub user_id: String, pub order_no: String, #[cfg_attr(feature = "export-types", ts(type = "number"))] pub subtotal: i64, diff --git a/src/dto/page.rs b/src/dto/page.rs new file mode 100644 index 00000000..cdeacf1b --- /dev/null +++ b/src/dto/page.rs @@ -0,0 +1,39 @@ +use serde::Serialize; +#[cfg(feature = "export-types")] +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::models::page::Page; + +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, ToSchema)] +pub struct PageResponse { + pub id: String, + pub title: String, + pub slug: String, + pub content: Option, + pub status: String, + pub template: String, + #[cfg_attr(feature = "export-types", ts(type = "number"))] + pub sort_order: i64, + pub cover_image: Option, + pub created_at: String, + pub updated_at: String, +} + +impl PageResponse { + pub fn from_page(p: Page) -> Self { + Self { + id: p.id.to_string(), + title: p.title, + slug: p.slug, + content: p.content, + status: p.status.to_string(), + template: p.template, + sort_order: p.sort_order, + cover_image: p.cover_image, + created_at: p.created_at.to_rfc3339(), + updated_at: p.updated_at.to_rfc3339(), + } + } +} diff --git a/src/dto/payment.rs b/src/dto/payment.rs index a79f3100..c7f5dec5 100644 --- a/src/dto/payment.rs +++ b/src/dto/payment.rs @@ -71,13 +71,11 @@ pub struct UpdatePaymentChannelRequest { #[derive(Debug, Serialize, ToSchema)] pub struct PaymentOrderResponse { pub id: String, - pub user_id: i64, pub order_id: Option, pub title: String, #[cfg_attr(feature = "export-types", ts(type = "number"))] pub amount: i64, pub currency: String, - pub channel_id: String, pub provider: String, pub provider_order_id: Option, pub provider_method: Option, @@ -124,7 +122,7 @@ pub struct PaymentChannelResponse { impl From for PaymentChannelResponse { fn from(ch: crate::models::payment_channel::PaymentChannel) -> Self { Self { - id: ch.document_id, + id: ch.id.to_string(), provider: ch.provider, name: ch.name, is_live: ch.is_live != 0, @@ -148,11 +146,7 @@ fn mask_credentials(_creds: &str) -> String { #[derive(Debug, Serialize, ToSchema)] pub struct PaymentTransactionResponse { pub id: String, - #[cfg_attr(feature = "export-types", ts(type = "number"))] - pub payment_order_id: i64, pub order_id: Option, - #[cfg_attr(feature = "export-types", ts(type = "number"))] - pub user_id: i64, pub tx_type: String, #[cfg_attr(feature = "export-types", ts(type = "number"))] pub amount: i64, @@ -165,10 +159,8 @@ pub struct PaymentTransactionResponse { impl From for PaymentTransactionResponse { fn from(tx: crate::models::payment_transaction::PaymentTransaction) -> Self { Self { - id: tx.document_id, - payment_order_id: tx.payment_order_id, + id: tx.id.to_string(), order_id: tx.order_id, - user_id: tx.user_id, tx_type: tx.tx_type, amount: tx.amount, currency: tx.currency, @@ -183,12 +175,8 @@ impl From for PaymentTra #[derive(Debug, Serialize, ToSchema)] pub struct PaymentRefundResponse { pub id: String, - #[cfg_attr(feature = "export-types", ts(type = "number"))] - pub payment_order_id: i64, pub order_id: Option, #[cfg_attr(feature = "export-types", ts(type = "number"))] - pub user_id: i64, - #[cfg_attr(feature = "export-types", ts(type = "number"))] pub amount: i64, pub currency: String, pub reason: Option, @@ -201,10 +189,8 @@ pub struct PaymentRefundResponse { impl From for PaymentRefundResponse { fn from(r: crate::models::payment_refund::PaymentRefund) -> Self { Self { - id: r.document_id, - payment_order_id: r.payment_order_id, + id: r.id.to_string(), order_id: r.order_id, - user_id: r.user_id, amount: r.amount, currency: r.currency, reason: r.reason, diff --git a/src/dto/post.rs b/src/dto/post.rs index db55bc65..1ed57960 100644 --- a/src/dto/post.rs +++ b/src/dto/post.rs @@ -50,9 +50,7 @@ pub struct PostResponse { pub excerpt: Option, pub cover_image: Option, pub status: PostStatus, - pub created_by: i64, pub author_name: Option, - pub category_id: Option, pub category_name: Option, pub tags: Vec, pub view_count: i64, @@ -76,6 +74,8 @@ pub struct PostResponse { pub published_at: Option, pub title_highlight: Option, pub excerpt_highlight: Option, + pub created_by: Option, + pub category_id: Option, } impl PostResponse { @@ -83,15 +83,13 @@ impl PostResponse { let status = p.status; let comment_status = p.comment_status; Ok(Self { - id: p.document_id, + id: p.id.to_string(), title: p.title, slug: p.slug, content: p.content, status, cover_image: p.cover_image, - created_by: p.created_by, author_name: None, - category_id: p.category_id, category_name: None, tags: vec![], view_count: p.view_count, @@ -113,6 +111,8 @@ impl PostResponse { published_at: p.published_at, title_highlight: None, excerpt_highlight: None, + created_by: None, + category_id: None, }) } } diff --git a/src/dto/reusable_block.rs b/src/dto/reusable_block.rs new file mode 100644 index 00000000..874bc4be --- /dev/null +++ b/src/dto/reusable_block.rs @@ -0,0 +1,28 @@ +use serde::Serialize; +#[cfg(feature = "export-types")] +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::models::reusable_block::ReusableBlock; + +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, ToSchema)] +pub struct ReusableBlockResponse { + pub id: String, + pub name: String, + pub content: String, + pub created_at: String, + pub updated_at: String, +} + +impl ReusableBlockResponse { + pub fn from_block(b: ReusableBlock) -> Self { + Self { + id: b.id.to_string(), + name: b.name, + content: b.content, + created_at: b.created_at.to_rfc3339(), + updated_at: b.updated_at.to_rfc3339(), + } + } +} diff --git a/src/dto/tag.rs b/src/dto/tag.rs index c5ef25b4..8fe0bd93 100644 --- a/src/dto/tag.rs +++ b/src/dto/tag.rs @@ -4,6 +4,30 @@ use ts_rs::TS; use utoipa::ToSchema; use validator::Validate; +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, ToSchema)] +pub struct TagResponse { + pub id: String, + pub name: String, + pub slug: String, + pub description: Option, + pub created_at: String, + pub updated_at: String, +} + +impl TagResponse { + pub fn from_tag(tag: crate::models::tag::Tag) -> Self { + Self { + id: tag.id.to_string(), + name: tag.name, + slug: tag.slug, + description: tag.description, + created_at: tag.created_at.to_rfc3339(), + updated_at: tag.updated_at.to_rfc3339(), + } + } +} + #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)] pub struct CreateTagRequest { diff --git a/src/dto/tenant.rs b/src/dto/tenant.rs new file mode 100644 index 00000000..eee91094 --- /dev/null +++ b/src/dto/tenant.rs @@ -0,0 +1,32 @@ +use serde::Serialize; +#[cfg(feature = "export-types")] +use ts_rs::TS; +use utoipa::ToSchema; + +use crate::models::tenant::Tenant; + +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, ToSchema)] +pub struct TenantResponse { + pub id: String, + pub name: String, + pub domain: Option, + pub status: String, + pub config: Option, + pub created_at: String, + pub updated_at: String, +} + +impl TenantResponse { + pub fn from_tenant(t: Tenant) -> Self { + Self { + id: t.id.to_string(), + name: t.name, + domain: t.domain, + status: t.status.to_string(), + config: Some(t.config), + created_at: t.created_at.to_rfc3339(), + updated_at: t.updated_at.to_rfc3339(), + } + } +} diff --git a/src/dto/user.rs b/src/dto/user.rs index c7ab8092..626f14ed 100644 --- a/src/dto/user.rs +++ b/src/dto/user.rs @@ -138,7 +138,7 @@ pub struct BindEmailRequest { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, ToSchema)] pub struct CredentialResponse { - pub id: i64, + pub id: String, pub auth_type: AuthType, pub identifier: String, pub verified: bool, @@ -151,7 +151,7 @@ pub struct CredentialResponse { impl CredentialResponse { pub fn from_credential(c: crate::models::user_credential::UserCredential) -> AppResult { Ok(Self { - id: c.id, + id: c.id.to_string(), auth_type: c.auth_type, identifier: c.identifier, verified: c.verified == 1, @@ -217,7 +217,7 @@ impl UserResponse { let status = user.status; let registered_via = user.registered_via; Ok(Self { - id: user.document_id, + id: user.id.to_string(), username: user.username, role, status, diff --git a/src/dto/wallet.rs b/src/dto/wallet.rs index ffa8c763..691e803f 100644 --- a/src/dto/wallet.rs +++ b/src/dto/wallet.rs @@ -29,7 +29,7 @@ impl WalletResponse { pub fn from_wallet(w: Wallet) -> AppResult { Ok(Self { status: w.status, - id: w.document_id, + id: w.id.to_string(), currency: w.currency, balance: w.balance, created_at: w.created_at, @@ -49,10 +49,10 @@ pub struct WalletTransactionResponse { pub tx_type: WalletTxType, pub currency: String, pub transaction_no: String, - pub related_tx_id: Option, pub reference_type: Option, pub reference_id: Option, pub metadata: Option, + pub related_tx_id: Option, #[schema(value_type = String)] pub created_at: Timestamp, } @@ -63,14 +63,14 @@ impl WalletTransactionResponse { entry_type: tx.entry_type, tx_type: tx.tx_type, reference_type: tx.reference_type, - id: tx.document_id, + id: tx.id.to_string(), amount: tx.amount, balance_after: tx.balance_after, currency: tx.currency, transaction_no: tx.transaction_no, - related_tx_id: None, reference_id: tx.reference_id, metadata: tx.metadata, + related_tx_id: None, created_at: tx.created_at, }) } diff --git a/src/event/mod.rs b/src/event/mod.rs index 8aa6ab10..73e67785 100644 --- a/src/event/mod.rs +++ b/src/event/mod.rs @@ -200,10 +200,10 @@ impl Event { let data = value.get("data")?; let subject_id = data - .get("document_id") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); + .get("id") + .and_then(|v| v.as_i64()) + .map(|id| id.to_string()) + .unwrap_or_default(); let actor_id = data .get("user_id") @@ -225,27 +225,27 @@ impl Event { action: "create".into(), subject: "post".into(), subject_id: data.id.clone(), - actor_id: Some(data.created_by), + actor_id: data.created_by.as_deref().and_then(|s| s.parse().ok()), detail: Some(format!("title={}", data.title)), }), Event::PostUpdated(data) => Some(AuditInfo { action: "update".into(), subject: "post".into(), - subject_id: data.document_id.clone(), + subject_id: data.id.to_string(), actor_id: None, detail: Some(format!("slug={}", data.slug)), }), Event::PostDeleted(data) => Some(AuditInfo { action: "delete".into(), subject: "post".into(), - subject_id: data.document_id.clone(), + subject_id: data.id.to_string(), actor_id: None, detail: Some(format!("slug={}", data.slug)), }), Event::CommentCreated(data) => Some(AuditInfo { action: "create".into(), subject: "comment".into(), - subject_id: data.document_id.clone(), + subject_id: data.id.to_string(), actor_id: None, detail: Some(format!( "author={}", @@ -255,35 +255,35 @@ impl Event { Event::UserRegistered(data) => Some(AuditInfo { action: "register".into(), subject: "user".into(), - subject_id: data.document_id.clone(), + subject_id: data.id.to_string(), actor_id: None, detail: Some(format!("username={}", data.username)), }), Event::UserLoggedIn { user, success } => Some(AuditInfo { action: "login".into(), subject: "user".into(), - subject_id: user.document_id.clone(), - actor_id: Some(user.id), + subject_id: user.id.to_string(), + actor_id: Some(*user.id), detail: Some(format!("success={}", success)), }), Event::MediaUploaded(data) => Some(AuditInfo { action: "upload".into(), subject: "media".into(), - subject_id: data.document_id.clone(), - actor_id: Some(data.user_id), + subject_id: data.id.to_string(), + actor_id: Some(*data.user_id), detail: Some(format!("filename={}", data.filename)), }), Event::MediaDeleted(data) => Some(AuditInfo { action: "delete".into(), subject: "media".into(), - subject_id: data.document_id.clone(), + subject_id: data.id.to_string(), actor_id: None, detail: None, }), Event::PasswordResetRequested { user, token: _ } => Some(AuditInfo { action: "password_reset_request".into(), subject: "user".into(), - subject_id: user.document_id.clone(), + subject_id: user.id.to_string(), actor_id: None, detail: Some(format!("username={}", user.username)), }), @@ -312,7 +312,7 @@ mod tests { excerpt: None, cover_image: None, status: crate::models::post::PostStatus::Draft, - created_by: 0, + created_by: None, author_name: None, category_id: None, category_name: None, diff --git a/src/eventbus.rs b/src/eventbus.rs index 97f2bab1..6ea2b1f5 100644 --- a/src/eventbus.rs +++ b/src/eventbus.rs @@ -68,7 +68,7 @@ mod tests { excerpt: None, cover_image: None, status: PostStatus::Published, - created_by: 0, + created_by: None, author_name: None, category_id: None, category_name: None, @@ -96,8 +96,7 @@ mod tests { fn make_post(id: i64, slug: &str) -> Post { Post { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, title: String::new(), slug: slug.into(), @@ -105,7 +104,7 @@ mod tests { excerpt: None, cover_image: None, status: PostStatus::Published, - created_by: 0, + created_by: crate::utils::id::SnowflakeId(0), updated_by: None, category_id: None, view_count: 0, @@ -129,8 +128,7 @@ mod tests { fn make_user(id: i64, username: &str) -> User { User { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, username: username.into(), role: UserRole::Reader, diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs index bd8f901e..1cb52160 100644 --- a/src/graphql/mutation.rs +++ b/src/graphql/mutation.rs @@ -51,8 +51,8 @@ impl MutationRoot { }; let save_ctx = SaveContext { - user_id: auth.user_id().map(|s| s.to_string()), - user_int_id: auth.user_int_id(), + user_id: auth.user_id().map(|id| id.to_string()), + user_int_id: auth.user_id(), user_role: Some(auth.role().to_string()), tenant_id: auth.tenant_id().map(|s| s.to_string()), }; @@ -86,8 +86,13 @@ impl MutationRoot { )?; let repo = ContentRepository::new(state.pool.clone()); + let int_id: i64 = id + .as_str() + .parse() + .map_err(|e| async_graphql::Error::new(format!("invalid id: {e}")))?; + let existing = repo - .find_by_id(&ct, id.as_str(), None, true) + .find_by_id(&ct, int_id, None, true) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; @@ -107,24 +112,18 @@ impl MutationRoot { }; let save_ctx = SaveContext { - user_id: auth.user_id().map(|s| s.to_string()), - user_int_id: auth.user_int_id(), + user_id: auth.user_id().map(|id| id.to_string()), + user_int_id: auth.user_id(), user_role: Some(auth.role().to_string()), tenant_id: auth.tenant_id().map(|s| s.to_string()), }; let result = repo - .update( - &ct, - id.as_str(), - serde_json::Value::Object(obj), - None, - &save_ctx, - ) + .update(&ct, int_id, serde_json::Value::Object(obj), None, &save_ctx) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; - let cache_key = cms_detail_cache_key(&ct, id.as_str()); + let cache_key = cms_detail_cache_key(&ct, int_id); state.cms_cache.remove(&cache_key); Ok(json_to_content_item(result)) @@ -149,8 +148,13 @@ impl MutationRoot { )?; let repo = ContentRepository::new(state.pool.clone()); + let int_id: i64 = id + .as_str() + .parse() + .map_err(|e| async_graphql::Error::new(format!("invalid id: {e}")))?; + let existing = repo - .find_by_id(&ct, id.as_str(), None, true) + .find_by_id(&ct, int_id, None, true) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; @@ -166,7 +170,7 @@ impl MutationRoot { repo.delete( &ct, - id.as_str(), + int_id, None, &state.protocol_registry, &state.content_type_registry, @@ -174,7 +178,7 @@ impl MutationRoot { .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; - let cache_key = cms_detail_cache_key(&ct, id.as_str()); + let cache_key = cms_detail_cache_key(&ct, int_id); state.cms_cache.remove(&cache_key); Ok(DeleteResult { diff --git a/src/graphql/query.rs b/src/graphql/query.rs index 044f57e4..0540001e 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -79,7 +79,11 @@ impl QueryRoot { async_graphql::Error::new(format!("content type '{}' not found", r#type)) })?; - let result = match do_get(&state, &ct, id.as_str(), &auth).await { + let int_id = id + .as_str() + .parse::() + .map_err(|_| async_graphql::Error::new("invalid id"))?; + let result = match do_get(&state, &ct, int_id, &auth).await { Ok(val) => val, Err(crate::errors::app_error::AppError::NotFound(_)) => return Ok(None), Err(e) => return Err(async_graphql::Error::new(e.to_string())), diff --git a/src/handlers/cart.rs b/src/handlers/cart.rs index 53803775..5ed9d9a4 100644 --- a/src/handlers/cart.rs +++ b/src/handlers/cart.rs @@ -67,8 +67,7 @@ fn to_order_response( items: Vec, ) -> OrderResponse { OrderResponse { - id: o.document_id, - user_id: o.user_id.to_string(), + id: o.id.to_string(), order_no: o.order_no, subtotal: o.subtotal, discount_amount: o.discount_amount, @@ -106,7 +105,7 @@ pub async fn add_to_cart( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; validation::validate(&req)?; state @@ -132,7 +131,7 @@ pub async fn list_cart( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; let cart = state.cart_service.list_items(&auth, user_int_id).await?; Ok(ApiResponse::success(cart)) @@ -152,12 +151,13 @@ pub async fn update_cart_item( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; state .cart_service - .update_quantity(&auth, &id, user_int_id, req.quantity) + .update_quantity(&auth, id, user_int_id, req.quantity) .await?; Ok(ApiResponse::success(())) } @@ -174,11 +174,12 @@ pub async fn remove_from_cart( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; state .cart_service - .remove_item(&auth, &id, user_int_id) + .remove_item(&auth, id, user_int_id) .await?; Ok(ApiResponse::success(())) } @@ -193,7 +194,7 @@ pub async fn clear_cart( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; state.cart_service.clear_cart(&auth, user_int_id).await?; Ok(ApiResponse::success(())) @@ -209,7 +210,7 @@ pub async fn checkout( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; let (order, items) = state.cart_service.checkout(&auth, user_int_id).await?; Ok(ApiResponse::success(to_order_response(order, items))) diff --git a/src/handlers/category.rs b/src/handlers/category.rs index c2e218f5..2cd9839b 100644 --- a/src/handlers/category.rs +++ b/src/handlers/category.rs @@ -3,7 +3,9 @@ use axum::Json; use axum::extract::{Path, Query, State}; -use crate::dto::{BatchRequest, BatchResponse, CreateCategoryRequest, UpdateCategoryRequest}; +use crate::dto::{ + BatchRequest, BatchResponse, CategoryResponse, CreateCategoryRequest, UpdateCategoryRequest, +}; use crate::errors::app_error::AppResult; use crate::errors::response::{ApiResponse, PaginatedData}; use crate::errors::validation; @@ -126,13 +128,16 @@ pub async fn list( auth: AuthUser, State(state): State, Query(mut params): Query, -) -> AppResult>> -{ +) -> AppResult>> { params.sanitize(); let (items, total) = state .category_service .list_paginated(&auth, params.page, params.page_size) .await?; + let items: Vec = items + .into_iter() + .map(CategoryResponse::from_category) + .collect(); Ok(params.paginate(items, total)) } @@ -145,9 +150,10 @@ pub async fn get( auth: AuthUser, State(state): State, Path(id): Path, -) -> AppResult> { - let cat = state.category_service.get(&id, &auth).await?; - Ok(ApiResponse::success(cat)) +) -> AppResult> { + let id = crate::utils::id::parse_id(&id)?; + let cat = state.category_service.get(id, &auth).await?; + Ok(ApiResponse::success(CategoryResponse::from_category(cat))) } /// Create a new category @@ -160,11 +166,11 @@ pub async fn create( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; let cat = state.category_service.create(&auth, req).await?; - Ok(ApiResponse::success(cat)) + Ok(ApiResponse::success(CategoryResponse::from_category(cat))) } /// Update a category @@ -179,11 +185,12 @@ pub async fn update( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; - let cat = state.category_service.update(&auth, &id, req).await?; - Ok(ApiResponse::success(cat)) + let id = crate::utils::id::parse_id(&id)?; + let cat = state.category_service.update(&auth, id, req).await?; + Ok(ApiResponse::success(CategoryResponse::from_category(cat))) } /// Delete a category @@ -198,7 +205,8 @@ pub async fn delete( Path(id): Path, ) -> AppResult> { auth.ensure_author()?; - state.category_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.category_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -208,13 +216,17 @@ pub async fn admin_list( auth: AuthUser, State(state): State, Query(mut params): Query, -) -> AppResult>> { +) -> AppResult>> { auth.ensure_admin()?; params.sanitize(); let (items, total) = state .category_service .list_paginated(&auth, params.page, params.page_size) .await?; + let items: Vec = items + .into_iter() + .map(CategoryResponse::from_category) + .collect(); Ok(params.paginate(items, total)) } @@ -222,11 +234,11 @@ pub async fn admin_create( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; let cat = state.category_service.create(&auth, req).await?; - Ok(ApiResponse::success(cat)) + Ok(ApiResponse::success(CategoryResponse::from_category(cat))) } pub async fn admin_update( @@ -234,11 +246,12 @@ pub async fn admin_update( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; - let cat = state.category_service.update(&auth, &id, req).await?; - Ok(ApiResponse::success(cat)) + let id = crate::utils::id::parse_id(&id)?; + let cat = state.category_service.update(&auth, id, req).await?; + Ok(ApiResponse::success(CategoryResponse::from_category(cat))) } pub async fn admin_delete( @@ -247,7 +260,8 @@ pub async fn admin_delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.category_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.category_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -260,8 +274,10 @@ pub async fn admin_batch( validation::validate(&req)?; let mut affected = 0usize; if req.action == "delete" { - for id in &req.ids { - if state.category_service.delete(id, &auth).await.is_ok() { + for raw_id in &req.ids { + if let Ok(id) = crate::utils::id::parse_id(raw_id) + && state.category_service.delete(id, &auth).await.is_ok() + { affected += 1; } } diff --git a/src/handlers/comment.rs b/src/handlers/comment.rs index 1a2d3346..4bac6370 100644 --- a/src/handlers/comment.rs +++ b/src/handlers/comment.rs @@ -253,7 +253,8 @@ pub async fn delete( State(state): State, Path(id): Path, ) -> AppResult> { - state.comment_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.comment_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -272,9 +273,10 @@ pub async fn update_status( ) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; state .comment_service - .update_status(&id, req.status, &auth) + .update_status(id, req.status, &auth) .await?; Ok(ApiResponse::success(())) } @@ -313,9 +315,10 @@ pub async fn admin_update_status( ) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; state .comment_service - .update_status(&id, req.status, &auth) + .update_status(id, req.status, &auth) .await?; Ok(ApiResponse::success(())) } @@ -331,7 +334,8 @@ pub async fn admin_delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.comment_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.comment_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -348,7 +352,10 @@ pub async fn admin_batch( auth.ensure_admin()?; validation::validate(&req)?; let mut affected = 0usize; - for id in &req.ids { + for raw_id in &req.ids { + let Ok(id) = crate::utils::id::parse_id(raw_id) else { + continue; + }; match req.action.as_str() { "delete" => { if state.comment_service.delete(id, &auth).await.is_ok() { diff --git a/src/handlers/content_revision.rs b/src/handlers/content_revision.rs index 9644aa7c..93c4fb53 100644 --- a/src/handlers/content_revision.rs +++ b/src/handlers/content_revision.rs @@ -35,8 +35,10 @@ pub async fn list_revisions( )); } + let int_id: i64 = crate::utils::id::parse_id(&id)?; + let (summaries, total) = - crate::services::content_revision::list_revisions(&state.pool, &ct.singular, &id, 0, 0) + crate::services::content_revision::list_revisions(&state.pool, &ct.singular, int_id, 0, 0) .await?; Ok(ApiResponse::success(json!({ @@ -66,12 +68,11 @@ pub async fn get_revision( )); } - let rev_id: i64 = revision_id - .parse() - .map_err(|e| AppError::BadRequest(format!("invalid revision_id: {e}")))?; + let rev_id: i64 = crate::utils::id::parse_id(&revision_id)?; + let int_id: i64 = crate::utils::id::parse_id(&id)?; let revision = - crate::services::content_revision::get_revision(&state.pool, &ct.singular, &id, rev_id) + crate::services::content_revision::get_revision(&state.pool, &ct.singular, int_id, rev_id) .await?; let snapshot: serde_json::Value = serde_json::from_str(&revision.snapshot) @@ -107,17 +108,20 @@ pub async fn restore_revision( )); } - let rev_id: i64 = revision_id - .parse() - .map_err(|e| AppError::BadRequest(format!("invalid revision_id: {e}")))?; + let rev_id: i64 = crate::utils::id::parse_id(&revision_id)?; + let int_id: i64 = crate::utils::id::parse_id(&id)?; - let snapshot = - crate::services::content_revision::restore_revision(&state.pool, &ct.singular, &id, rev_id) - .await?; + let snapshot = crate::services::content_revision::restore_revision( + &state.pool, + &ct.singular, + int_id, + rev_id, + ) + .await?; let repo = ContentRepository::new(state.pool.clone()); let result = repo - .update(&ct, &id, snapshot, None, &Default::default()) + .update(&ct, int_id, snapshot, None, &Default::default()) .await?; let value = serde_json::to_value(result) @@ -146,17 +150,14 @@ pub async fn diff_revisions( )); } - let rev_a_id: i64 = rev_a - .parse() - .map_err(|e| AppError::BadRequest(format!("invalid revision_id: {e}")))?; - let rev_b_id: i64 = rev_b - .parse() - .map_err(|e| AppError::BadRequest(format!("invalid revision_id: {e}")))?; + let rev_a_id: i64 = crate::utils::id::parse_id(&rev_a)?; + let rev_b_id: i64 = crate::utils::id::parse_id(&rev_b)?; + let int_id: i64 = crate::utils::id::parse_id(&id)?; let result = crate::services::content_revision::diff_revisions( &state.pool, &ct.singular, - &id, + int_id, rev_a_id, rev_b_id, ) diff --git a/src/handlers/cron.rs b/src/handlers/cron.rs index d4c65729..0cf66eb5 100644 --- a/src/handlers/cron.rs +++ b/src/handlers/cron.rs @@ -193,7 +193,8 @@ pub async fn get( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - let schedule = find_by_id(&state.pool, &id) + let id: i64 = crate::utils::id::parse_id(&id)?; + let schedule = find_by_id(&state.pool, id) .await? .ok_or_else(|| AppError::not_found("cron_schedule"))?; Ok(ApiResponse::success(schedule)) @@ -239,9 +240,10 @@ pub async fn update( ) -> AppResult> { auth.ensure_admin()?; crate::errors::validation::validate(&req)?; + let id: i64 = crate::utils::id::parse_id(&id)?; let updated = update_schedule( &state.pool, - &id, + id, req.label, req.job_type, req.payload, @@ -266,7 +268,8 @@ pub async fn toggle( Json(body): Json, ) -> AppResult> { auth.ensure_admin()?; - toggle_schedule(&state.pool, &id, body.enabled).await?; + let id: i64 = crate::utils::id::parse_id(&id)?; + toggle_schedule(&state.pool, id, body.enabled).await?; Ok(ApiResponse::success(())) } @@ -282,7 +285,8 @@ pub async fn delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - delete_schedule(&state.pool, &id).await?; + let id: i64 = crate::utils::id::parse_id(&id)?; + delete_schedule(&state.pool, id).await?; Ok(ApiResponse::success(())) } @@ -303,7 +307,8 @@ pub async fn logs( auth.ensure_admin()?; let limit = params.limit.clamp(1, 100); let logs = if let Some(ref schedule_id) = params.schedule_id { - list_execution_logs(&state.pool, schedule_id, limit).await? + let sid: i64 = crate::utils::id::parse_id(schedule_id)?; + list_execution_logs(&state.pool, sid, limit).await? } else { recent_execution_logs(&state.pool, limit).await? }; @@ -343,7 +348,11 @@ pub async fn admin_batch( auth.ensure_admin()?; crate::errors::validation::validate(&req)?; let mut affected = 0usize; - for id in &req.ids { + for id_str in &req.ids { + let id: i64 = match id_str.parse() { + Ok(v) => v, + Err(_) => continue, + }; match req.action.as_str() { "delete" => { if delete_schedule(&state.pool, id).await.is_ok() { diff --git a/src/handlers/order.rs b/src/handlers/order.rs index 29006c73..f84ae875 100644 --- a/src/handlers/order.rs +++ b/src/handlers/order.rs @@ -154,8 +154,7 @@ fn to_order_response( items: Vec, ) -> OrderResponse { OrderResponse { - id: o.document_id, - user_id: o.user_id.to_string(), + id: o.id.to_string(), order_no: o.order_no, subtotal: o.subtotal, discount_amount: o.discount_amount, @@ -193,7 +192,7 @@ pub async fn create_order( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; validation::validate(&req)?; let (o, items) = state.order_service.create(&auth, user_int_id, req).await?; @@ -211,7 +210,7 @@ pub async fn list_orders( ) -> AppResult>> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; params.sanitize(); let (orders, total) = state @@ -236,7 +235,8 @@ pub async fn get_order( Path(id): Path, ) -> AppResult> { auth.ensure_authenticated()?; - let (o, items) = state.order_service.get(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + let (o, items) = state.order_service.get(&auth, id).await?; Ok(ApiResponse::success(to_order_response(o, items))) } @@ -254,9 +254,10 @@ pub async fn cancel_order_handler( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; - state.order_service.cancel(&auth, &id, user_int_id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.order_service.cancel(&auth, id, user_int_id).await?; Ok(ApiResponse::success(())) } @@ -272,11 +273,12 @@ pub async fn confirm_receipt( ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; let user_int_id = auth - .user_int_id() + .user_id() .ok_or(crate::errors::app_error::AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; state .order_service - .confirm_receipt(&auth, &id, user_int_id) + .confirm_receipt(&auth, id, user_int_id) .await?; Ok(ApiResponse::success(())) } @@ -314,7 +316,8 @@ pub async fn admin_get( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - let (o, items) = state.order_service.get(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + let (o, items) = state.order_service.get(&auth, id).await?; Ok(ApiResponse::success(to_order_response(o, items))) } @@ -331,7 +334,8 @@ pub async fn admin_ship( Json(req): Json, ) -> AppResult> { auth.ensure_admin()?; - state.order_service.ship(&auth, &id, &req).await?; + let id = crate::utils::id::parse_id(&id)?; + state.order_service.ship(&auth, id, &req).await?; Ok(ApiResponse::success(())) } @@ -346,7 +350,8 @@ pub async fn admin_cancel( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.order_service.admin_cancel(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.order_service.admin_cancel(&auth, id).await?; Ok(ApiResponse::success(())) } @@ -361,7 +366,8 @@ pub async fn admin_pay( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.order_service.mark_paid(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.order_service.mark_paid(&auth, id).await?; Ok(ApiResponse::success(())) } @@ -376,7 +382,8 @@ pub async fn admin_refund( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.order_service.refund(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.order_service.refund(&auth, id).await?; Ok(ApiResponse::success(())) } @@ -393,9 +400,10 @@ pub async fn admin_update_remark( Json(req): Json, ) -> AppResult> { auth.ensure_admin()?; + let id = crate::utils::id::parse_id(&id)?; state .order_service - .update_admin_remark(&auth, &id, &req.admin_remark) + .update_admin_remark(&auth, id, &req.admin_remark) .await?; Ok(ApiResponse::success(())) } diff --git a/src/handlers/page.rs b/src/handlers/page.rs index 3d97f811..74a0422b 100644 --- a/src/handlers/page.rs +++ b/src/handlers/page.rs @@ -10,13 +10,13 @@ use ts_rs::TS; use validator::Validate; use crate::commands::{CreatePageCmd, UpdatePageCmd}; -use crate::dto::{BatchRequest, BatchResponse}; +use crate::dto::{BatchRequest, BatchResponse, PageResponse}; use crate::errors::app_error::{AppError, AppResult}; use crate::errors::response::{ApiResponse, PaginatedData}; use crate::errors::validation; use crate::middleware::auth::AuthUser; use crate::models::page::PageStatus; -use crate::services::post::resolve_doc_id_to_int; +use crate::services::post::find_existing_id; use crate::utils::pagination::PaginationParams; pub fn routes( @@ -151,10 +151,11 @@ async fn resolve_page_parent_id( pool: &crate::db::Pool, parent_id: Option, ) -> AppResult> { - let Some(doc_id) = parent_id else { + let Some(raw_id) = parent_id else { return Ok(None); }; - resolve_doc_id_to_int(pool, "pages", &doc_id, None).await + let parsed_id = crate::utils::id::parse_id(&raw_id)?; + find_existing_id(pool, "pages", parsed_id, None).await } // ── DTO ── @@ -245,7 +246,7 @@ pub async fn list( auth: AuthUser, State(state): State, Query(query): Query, -) -> AppResult>> { +) -> AppResult>> { let pagination = PaginationParams::from_options(query.page, query.page_size); let (items, total) = state @@ -253,6 +254,7 @@ pub async fn list( .list_published(pagination.page, pagination.page_size, &auth) .await?; + let items: Vec = items.into_iter().map(PageResponse::from_page).collect(); Ok(pagination.paginate(items, total)) } @@ -264,9 +266,9 @@ pub async fn get_by_slug( auth: AuthUser, State(state): State, Path(slug): Path, -) -> AppResult> { +) -> AppResult> { let page = state.page_service.get_by_slug(&slug, &auth).await?; - Ok(ApiResponse::success(page)) + Ok(ApiResponse::success(PageResponse::from_page(page))) } #[utoipa::path(get, path = "/pages/sitemap", tag = "pages", @@ -296,7 +298,7 @@ pub async fn admin_list( auth: AuthUser, State(state): State, Query(query): Query, -) -> AppResult>> { +) -> AppResult>> { auth.ensure_author()?; let pagination = PaginationParams::from_options(query.page, query.page_size); @@ -305,6 +307,7 @@ pub async fn admin_list( .list_all(pagination.page, pagination.page_size, query.status, &auth) .await?; + let items: Vec = items.into_iter().map(PageResponse::from_page).collect(); Ok(pagination.paginate(items, total)) } @@ -317,10 +320,11 @@ pub async fn admin_get( auth: AuthUser, State(state): State, Path(id): Path, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; - let page = state.page_service.get_by_id(&id, &auth).await?; - Ok(ApiResponse::success(page)) + let id = crate::utils::id::parse_id(&id)?; + let page = state.page_service.get_by_id(id, &auth).await?; + Ok(ApiResponse::success(PageResponse::from_page(page))) } #[utoipa::path(post, path = "/pages", tag = "pages", @@ -331,7 +335,7 @@ pub async fn create( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; @@ -354,13 +358,13 @@ pub async fn create( parent_id: resolved_parent_id, sort_order: req.sort_order.unwrap_or(0), status, - created_by: auth.user_int_id().ok_or(AppError::Unauthorized)?, + created_by: auth.user_id().ok_or(AppError::Unauthorized)?, updated_by: None, cover_image: req.cover_image, }; let page = state.page_service.create_page(&auth, cmd).await?; - Ok(ApiResponse::success(page)) + Ok(ApiResponse::success(PageResponse::from_page(page))) } #[utoipa::path(put, path = "/admin/pages/{id}", tag = "pages", @@ -373,7 +377,7 @@ pub async fn update( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; @@ -392,11 +396,12 @@ pub async fn update( sort_order: req.sort_order, status: req.status, cover_image: req.cover_image, - updated_by: auth.user_int_id(), + updated_by: auth.user_id(), }; - let page = state.page_service.update_page(&auth, &id, cmd).await?; - Ok(ApiResponse::success(page)) + let id = crate::utils::id::parse_id(&id)?; + let page = state.page_service.update_page(&auth, id, cmd).await?; + Ok(ApiResponse::success(PageResponse::from_page(page))) } #[utoipa::path(delete, path = "/admin/pages/{id}", tag = "pages", @@ -410,7 +415,8 @@ pub async fn delete( Path(id): Path, ) -> AppResult> { auth.ensure_author()?; - state.page_service.delete_page(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.page_service.delete_page(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -424,13 +430,14 @@ pub async fn update_status( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; + let id = crate::utils::id::parse_id(&id)?; let page = state .page_service - .update_status(&id, req.status, &auth) + .update_status(id, req.status, &auth) .await?; - Ok(ApiResponse::success(page)) + Ok(ApiResponse::success(PageResponse::from_page(page))) } #[utoipa::path(put, path = "/admin/pages/reorder", tag = "pages", @@ -465,7 +472,10 @@ pub async fn admin_batch( auth.ensure_admin()?; validation::validate(&req)?; let mut affected = 0usize; - for id in &req.ids { + for raw_id in &req.ids { + let Ok(id) = crate::utils::id::parse_id(raw_id) else { + continue; + }; match req.action.as_str() { "delete" => { if state.page_service.delete_page(id, &auth).await.is_ok() { diff --git a/src/handlers/payment.rs b/src/handlers/payment.rs index 732ed1f8..25d4b64d 100644 --- a/src/handlers/payment.rs +++ b/src/handlers/payment.rs @@ -90,11 +90,11 @@ pub fn routes( let mr = axum::routing::post(handle_callback).layer(axum::middleware::from_fn( crate::middleware::rate_limit::payment_callback_rate_limit, )); - r.route("/payment/callback/{channel_doc_id}", mr) + r.route("/payment/callback/{channel_id}", mr) }; registry.record( "POST", - "/api/v1/payment/callback/{channel_doc_id}", + "/api/v1/payment/callback/{channel_id}", "system public", "payment", ); @@ -202,13 +202,11 @@ pub fn routes( fn to_order_response(o: crate::models::payment_order::PaymentOrder) -> PaymentOrderResponse { PaymentOrderResponse { - id: o.document_id, - user_id: o.user_id, + id: o.id.to_string(), order_id: o.order_id, title: o.title, amount: o.amount, currency: o.currency, - channel_id: o.channel_id.to_string(), provider: o.provider, provider_order_id: o.provider_order_id, provider_method: o.provider_method, @@ -258,7 +256,7 @@ pub async fn create_payment_order_handler( Json(req): Json, ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; validation::validate(&req)?; let client_ip = extract_client_ip(&headers); let client_language = extract_accept_language(&headers); @@ -290,7 +288,7 @@ pub async fn list_user_orders( Query(mut params): Query, ) -> AppResult>> { let _user_id = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; params.sanitize(); let (orders, total) = state .payment_service @@ -311,10 +309,11 @@ pub async fn get_payment_order_handler( Path(id): Path, ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; let order = state .payment_service - .get_payment_order(&auth, user_int_id, &id) + .get_payment_order(&auth, user_int_id, id) .await?; Ok(ApiResponse::success(to_order_response(order))) } @@ -330,10 +329,11 @@ pub async fn cancel_payment_order_handler( Path(id): Path, ) -> AppResult> { let _user_id = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; state .payment_service - .cancel_payment_order(&auth, &id, user_int_id) + .cancel_payment_order(&auth, id, user_int_id) .await?; Ok(ApiResponse::success(())) } @@ -349,10 +349,11 @@ pub async fn list_order_transactions( Path(id): Path, ) -> AppResult>> { let _ = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; let txs = state .payment_service - .list_order_transactions(&auth, user_int_id, &id) + .list_order_transactions(&auth, user_int_id, id) .await?; let responses: Vec = txs.into_iter().map(Into::into).collect(); Ok(ApiResponse::success(responses)) @@ -369,29 +370,30 @@ pub async fn list_order_refunds( Path(id): Path, ) -> AppResult>> { let _ = auth.ensure_authenticated()?; - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_int_id = auth.user_id().ok_or(AppError::Unauthorized)?; + let id = crate::utils::id::parse_id(&id)?; let refunds = state .payment_service - .list_order_refunds(&auth, user_int_id, &id) + .list_order_refunds(&auth, user_int_id, id) .await?; let responses: Vec = refunds.into_iter().map(Into::into).collect(); Ok(ApiResponse::success(responses)) } -#[utoipa::path(post, path = "/payment/callback/{channel_doc_id}", tag = "payments", - params(("channel_doc_id" = String, Path, description = "Channel document ID")), +#[utoipa::path(post, path = "/payment/callback/{channel_id}", tag = "payments", + params(("channel_id" = String, Path, description = "Channel ID")), request_body = String, responses((status = 200, description = "Callback processed")) )] pub async fn handle_callback( State(state): State, - Path(channel_doc_id): Path, + Path(channel_id): Path, headers: HeaderMap, body: Bytes, ) -> AppResult> { state .payment_service - .handle_callback(&channel_doc_id, &headers, &body) + .handle_callback(&channel_id, &headers, &body) .await?; Ok(ApiResponse::success(())) } @@ -490,7 +492,8 @@ pub async fn admin_get_channel( State(state): State, Path(id): Path, ) -> AppResult> { - let channel = state.payment_service.get_channel(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + let channel = state.payment_service.get_channel(&auth, id).await?; Ok(ApiResponse::success(PaymentChannelResponse::from(channel))) } @@ -507,10 +510,8 @@ pub async fn admin_update_channel( Json(req): Json, ) -> AppResult> { validation::validate(&req)?; - let channel = state - .payment_service - .update_channel(&auth, &id, req) - .await?; + let id = crate::utils::id::parse_id(&id)?; + let channel = state.payment_service.update_channel(&auth, id, req).await?; Ok(ApiResponse::success(PaymentChannelResponse::from(channel))) } @@ -524,7 +525,8 @@ pub async fn admin_delete_channel( State(state): State, Path(id): Path, ) -> AppResult> { - state.payment_service.delete_channel(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.payment_service.delete_channel(&auth, id).await?; Ok(ApiResponse::success(())) } @@ -558,9 +560,10 @@ pub async fn admin_get_order( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; + let id = crate::utils::id::parse_id(&id)?; let order = state .payment_service - .get_payment_order(&auth, 0, &id) + .get_payment_order(&auth, 0, id) .await?; Ok(ApiResponse::success(to_order_response(order))) } @@ -579,9 +582,10 @@ pub async fn admin_refund_order( ) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; let refund = state .payment_service - .refund_payment_order(&auth, &id, req) + .refund_payment_order(&auth, id, req) .await?; Ok(ApiResponse::success(PaymentRefundResponse::from(refund))) } diff --git a/src/handlers/post.rs b/src/handlers/post.rs index ca74a0a8..4d89f09f 100644 --- a/src/handlers/post.rs +++ b/src/handlers/post.rs @@ -183,12 +183,14 @@ pub async fn list( let pagination = PaginationParams::from_options(query.page, query.page_size); let cat_id = if let Some(ref cid) = query.category_id { - post_service::resolve_doc_id_to_int(&state.pool, "categories", cid, None).await? + let parsed = crate::utils::id::parse_id(cid)?; + post_service::find_existing_id(&state.pool, "categories", parsed, None).await? } else { None }; let tg_id = if let Some(ref tid) = query.tag_id { - post_service::resolve_doc_id_to_int(&state.pool, "tags", tid, None).await? + let parsed = crate::utils::id::parse_id(tid)?; + post_service::find_existing_id(&state.pool, "tags", parsed, None).await? } else { None }; diff --git a/src/handlers/product.rs b/src/handlers/product.rs index 12a9f494..4fa87c90 100644 --- a/src/handlers/product.rs +++ b/src/handlers/product.rs @@ -103,7 +103,8 @@ pub async fn get_product( State(state): State, Path(id): Path, ) -> AppResult> { - let p = state.product_service.get(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + let p = state.product_service.get(id, &auth).await?; Ok(ApiResponse::success(ProductResponse::from(p))) } @@ -156,7 +157,8 @@ pub async fn admin_update( ) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; - let p = state.product_service.update(&auth, &id, req).await?; + let id = crate::utils::id::parse_id(&id)?; + let p = state.product_service.update(&auth, id, req).await?; Ok(ApiResponse::success(ProductResponse::from(p))) } @@ -171,6 +173,7 @@ pub async fn admin_delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.product_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.product_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } diff --git a/src/handlers/product_variant.rs b/src/handlers/product_variant.rs index 9cc8783f..53cc6c8a 100644 --- a/src/handlers/product_variant.rs +++ b/src/handlers/product_variant.rs @@ -88,10 +88,8 @@ pub async fn admin_update( ) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; - let v = state - .product_variant_service - .update(&auth, &id, req) - .await?; + let id = crate::utils::id::parse_id(&id)?; + let v = state.product_variant_service.update(&auth, id, req).await?; Ok(ApiResponse::success(ProductVariantResponse::from(v))) } @@ -101,6 +99,7 @@ pub async fn admin_delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.product_variant_service.delete(&auth, &id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.product_variant_service.delete(&auth, id).await?; Ok(ApiResponse::success(())) } diff --git a/src/handlers/rbac.rs b/src/handlers/rbac.rs index cc41dd2c..67fcdcf8 100644 --- a/src/handlers/rbac.rs +++ b/src/handlers/rbac.rs @@ -129,7 +129,8 @@ pub async fn update_role( Path(id): Path, Json(req): Json, ) -> AppResult> { - let role = state.rbac.update_role(&id, &req).await?; + let id = crate::utils::id::parse_id(&id)?; + let role = state.rbac.update_role(id, &req).await?; Ok(ApiResponse::success(role)) } @@ -143,7 +144,8 @@ pub async fn delete_role( State(state): State, Path(id): Path, ) -> AppResult> { - state.rbac.delete_role(&id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.rbac.delete_role(id).await?; Ok(ApiResponse::success(serde_json::json!({"deleted": true}))) } @@ -191,7 +193,10 @@ pub async fn admin_batch( crate::errors::validation::validate(&req)?; let mut affected = 0usize; if req.action == "delete" { - for id in &req.ids { + for raw_id in &req.ids { + let Ok(id) = crate::utils::id::parse_id(raw_id) else { + continue; + }; if state.rbac.delete_role(id).await.is_ok() { affected += 1; } diff --git a/src/handlers/reusable_block.rs b/src/handlers/reusable_block.rs index 4813a38c..481fce39 100644 --- a/src/handlers/reusable_block.rs +++ b/src/handlers/reusable_block.rs @@ -5,7 +5,7 @@ use axum::extract::{Path, State}; use serde::Deserialize; use validator::Validate; -use crate::dto::{BatchRequest, BatchResponse}; +use crate::dto::{BatchRequest, BatchResponse, ReusableBlockResponse}; use crate::errors::app_error::{AppError, AppResult}; use crate::errors::response::ApiResponse; use crate::errors::validation; @@ -109,9 +109,13 @@ pub struct UpdateReusableRequest { pub async fn list_reusable( auth: AuthUser, State(state): State, -) -> AppResult>> { +) -> AppResult>> { auth.ensure_author()?; let items = reusable_service::list_reusable(&state.pool, &auth).await?; + let items: Vec = items + .into_iter() + .map(ReusableBlockResponse::from_block) + .collect(); Ok(ApiResponse::success(items)) } @@ -124,12 +128,15 @@ pub async fn get_reusable( auth: AuthUser, State(state): State, Path(id): Path, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; - let block = reusable_service::get_reusable(&state.pool, &id, &auth) + let id = crate::utils::id::parse_id(&id)?; + let block = reusable_service::get_reusable(&state.pool, id, &auth) .await? .ok_or_else(|| AppError::not_found("reusable_block"))?; - Ok(ApiResponse::success(block)) + Ok(ApiResponse::success(ReusableBlockResponse::from_block( + block, + ))) } #[utoipa::path(post, path = "/admin/reusable-blocks", tag = "reusable_blocks", @@ -140,7 +147,7 @@ pub async fn create_reusable( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; let block = reusable_service::create_reusable( @@ -152,7 +159,9 @@ pub async fn create_reusable( req.description.as_deref(), ) .await?; - Ok(ApiResponse::success(block)) + Ok(ApiResponse::success(ReusableBlockResponse::from_block( + block, + ))) } #[utoipa::path(put, path = "/admin/reusable-blocks/{id}", tag = "reusable_blocks", @@ -165,12 +174,13 @@ pub async fn update_reusable( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; let block = reusable_service::update_reusable( &state.pool, - &id, + id, &auth, req.name.as_deref(), req.block_type.as_deref(), @@ -178,7 +188,9 @@ pub async fn update_reusable( req.description.as_deref(), ) .await?; - Ok(ApiResponse::success(block)) + Ok(ApiResponse::success(ReusableBlockResponse::from_block( + block, + ))) } #[utoipa::path(delete, path = "/admin/reusable-blocks/{id}", tag = "reusable_blocks", @@ -192,7 +204,8 @@ pub async fn delete_reusable( Path(id): Path, ) -> AppResult> { auth.ensure_author()?; - reusable_service::delete_reusable(&state.pool, &id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + reusable_service::delete_reusable(&state.pool, id, &auth).await?; Ok(ApiResponse::success(())) } @@ -210,10 +223,11 @@ pub async fn admin_batch( validation::validate(&req)?; let mut affected = 0usize; if req.action == "delete" { - for id in &req.ids { - if reusable_service::delete_reusable(&state.pool, id, &auth) - .await - .is_ok() + for raw_id in &req.ids { + if let Ok(id) = crate::utils::id::parse_id(raw_id) + && reusable_service::delete_reusable(&state.pool, id, &auth) + .await + .is_ok() { affected += 1; } diff --git a/src/handlers/sse.rs b/src/handlers/sse.rs index 9270a8f7..5f17da1a 100644 --- a/src/handlers/sse.rs +++ b/src/handlers/sse.rs @@ -123,7 +123,7 @@ mod tests { excerpt: None, cover_image: None, status: PostStatus::Published, - created_by: 0, + created_by: None, author_name: None, category_id: None, category_name: None, @@ -151,8 +151,7 @@ mod tests { fn make_post(id: i64, slug: &str) -> Post { Post { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, title: String::new(), slug: slug.into(), @@ -160,7 +159,7 @@ mod tests { excerpt: None, cover_image: None, status: PostStatus::Published, - created_by: 0, + created_by: crate::utils::id::SnowflakeId(0), updated_by: None, category_id: None, view_count: 0, @@ -184,10 +183,9 @@ mod tests { fn make_comment(id: i64) -> Comment { Comment { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, - post_id: 0, + post_id: crate::utils::id::SnowflakeId(0), created_by: None, updated_by: None, nickname: None, @@ -204,8 +202,7 @@ mod tests { fn make_user(id: i64, username: &str) -> User { User { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, username: username.into(), role: UserRole::Reader, @@ -226,10 +223,9 @@ mod tests { fn make_media(id: i64, filename: &str) -> Media { Media { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, - user_id: 1, + user_id: crate::utils::id::SnowflakeId(1), filename: filename.into(), filepath: String::new(), mimetype: String::new(), @@ -247,9 +243,8 @@ mod tests { fn make_password_reset_token(user_id: i64) -> PasswordResetToken { PasswordResetToken { - id: 1, - document_id: "prt-1".into(), - user_id, + id: crate::utils::id::SnowflakeId(1), + user_id: crate::utils::id::SnowflakeId(user_id), token: "reset-token".into(), expires_at: ts(), used_at: None, @@ -259,9 +254,8 @@ mod tests { fn make_email_verification_token(user_id: i64, email: &str) -> EmailVerificationToken { EmailVerificationToken { - id: 1, - document_id: "evt-1".into(), - user_id, + id: crate::utils::id::SnowflakeId(1), + user_id: crate::utils::id::SnowflakeId(user_id), token: "verify-token".into(), email: email.into(), expires_at: ts(), diff --git a/src/handlers/tag.rs b/src/handlers/tag.rs index c354c88d..c76c32ea 100644 --- a/src/handlers/tag.rs +++ b/src/handlers/tag.rs @@ -3,7 +3,7 @@ use axum::Json; use axum::extract::{Path, Query, State}; -use crate::dto::{BatchRequest, BatchResponse, CreateTagRequest, UpdateTagRequest}; +use crate::dto::{BatchRequest, BatchResponse, CreateTagRequest, TagResponse, UpdateTagRequest}; use crate::errors::app_error::AppResult; use crate::errors::response::{ApiResponse, PaginatedData}; use crate::errors::validation; @@ -126,12 +126,13 @@ pub async fn list( auth: AuthUser, State(state): State, Query(mut params): Query, -) -> AppResult>> { +) -> AppResult>> { params.sanitize(); let (items, total) = state .tag_service .list_paginated(&auth, params.page, params.page_size) .await?; + let items: Vec = items.into_iter().map(TagResponse::from_tag).collect(); Ok(params.paginate(items, total)) } @@ -144,9 +145,10 @@ pub async fn get( auth: AuthUser, State(state): State, Path(id): Path, -) -> AppResult> { - let t = state.tag_service.get(&id, &auth).await?; - Ok(ApiResponse::success(t)) +) -> AppResult> { + let id = crate::utils::id::parse_id(&id)?; + let t = state.tag_service.get(id, &auth).await?; + Ok(ApiResponse::success(TagResponse::from_tag(t))) } /// Create a new tag @@ -159,11 +161,11 @@ pub async fn create( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; let t = state.tag_service.create(&auth, req).await?; - Ok(ApiResponse::success(t)) + Ok(ApiResponse::success(TagResponse::from_tag(t))) } /// Delete a tag @@ -178,7 +180,8 @@ pub async fn delete( Path(id): Path, ) -> AppResult> { auth.ensure_author()?; - state.tag_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.tag_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -194,15 +197,16 @@ pub async fn update( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_author()?; validation::validate(&req)?; let slug = crate::services::tag::generate_slug(&req.name); + let id = crate::utils::id::parse_id(&id)?; let t = state .tag_service - .update(&auth, &id, req.name.clone(), slug) + .update(&auth, id, req.name.clone(), slug) .await?; - Ok(ApiResponse::success(t)) + Ok(ApiResponse::success(TagResponse::from_tag(t))) } // ── Admin handlers ── @@ -211,13 +215,14 @@ pub async fn admin_list( auth: AuthUser, State(state): State, Query(mut params): Query, -) -> AppResult>> { +) -> AppResult>> { auth.ensure_admin()?; params.sanitize(); let (items, total) = state .tag_service .list_paginated(&auth, params.page, params.page_size) .await?; + let items: Vec = items.into_iter().map(TagResponse::from_tag).collect(); Ok(params.paginate(items, total)) } @@ -225,11 +230,11 @@ pub async fn admin_create( auth: AuthUser, State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; let t = state.tag_service.create(&auth, req).await?; - Ok(ApiResponse::success(t)) + Ok(ApiResponse::success(TagResponse::from_tag(t))) } pub async fn admin_update( @@ -237,15 +242,16 @@ pub async fn admin_update( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { +) -> AppResult> { auth.ensure_admin()?; validation::validate(&req)?; let slug = crate::services::tag::generate_slug(&req.name); + let id = crate::utils::id::parse_id(&id)?; let t = state .tag_service - .update(&auth, &id, req.name.clone(), slug) + .update(&auth, id, req.name.clone(), slug) .await?; - Ok(ApiResponse::success(t)) + Ok(ApiResponse::success(TagResponse::from_tag(t))) } pub async fn admin_delete( @@ -254,7 +260,8 @@ pub async fn admin_delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.tag_service.delete(&id, &auth).await?; + let id = crate::utils::id::parse_id(&id)?; + state.tag_service.delete(id, &auth).await?; Ok(ApiResponse::success(())) } @@ -267,8 +274,10 @@ pub async fn admin_batch( validation::validate(&req)?; let mut affected = 0usize; if req.action == "delete" { - for id in &req.ids { - if state.tag_service.delete(id, &auth).await.is_ok() { + for raw_id in &req.ids { + if let Ok(id) = crate::utils::id::parse_id(raw_id) + && state.tag_service.delete(id, &auth).await.is_ok() + { affected += 1; } } diff --git a/src/handlers/tenant.rs b/src/handlers/tenant.rs index aad2a390..bd787da5 100644 --- a/src/handlers/tenant.rs +++ b/src/handlers/tenant.rs @@ -4,10 +4,9 @@ use axum::Json; use axum::extract::{Path, Query, State}; use crate::AppState; -use crate::dto::{BatchRequest, BatchResponse}; +use crate::dto::{BatchRequest, BatchResponse, TenantResponse}; use crate::errors::app_error::{AppError, AppResult}; use crate::errors::response::ApiResponse; -use crate::models::tenant::Tenant; use crate::services::tenant::{CreateTenantRequest, UpdateTenantRequest}; use crate::utils::pagination::PaginationParams; @@ -87,9 +86,10 @@ pub fn routes( pub async fn list_tenants( State(state): State, Query(mut params): Query, -) -> AppResult>> { +) -> AppResult>> { params.sanitize(); let all = state.tenant.list().await?; + let all: Vec = all.into_iter().map(TenantResponse::from_tenant).collect(); Ok(params.paginate_in_memory(all)) } @@ -102,13 +102,14 @@ pub async fn list_tenants( pub async fn get_tenant( State(state): State, Path(id): Path, -) -> AppResult> { +) -> AppResult> { + let id = crate::utils::id::parse_id(&id)?; let tenant = state .tenant - .get(&id) + .get(id) .await? .ok_or_else(|| AppError::not_found(&format!("tenant/{id}")))?; - Ok(ApiResponse::success(tenant)) + Ok(ApiResponse::success(TenantResponse::from_tenant(tenant))) } /// POST /admin/tenants — Create a tenant @@ -119,9 +120,9 @@ pub async fn get_tenant( pub async fn create_tenant( State(state): State, Json(req): Json, -) -> AppResult> { +) -> AppResult> { let tenant = state.tenant.create(&req).await?; - Ok(ApiResponse::success(tenant)) + Ok(ApiResponse::success(TenantResponse::from_tenant(tenant))) } /// PUT /admin/tenants/:id — Update a tenant @@ -134,9 +135,10 @@ pub async fn update_tenant( State(state): State, Path(id): Path, Json(req): Json, -) -> AppResult> { - let tenant = state.tenant.update(&id, &req).await?; - Ok(ApiResponse::success(tenant)) +) -> AppResult> { + let id = crate::utils::id::parse_id(&id)?; + let tenant = state.tenant.update(id, &req).await?; + Ok(ApiResponse::success(TenantResponse::from_tenant(tenant))) } /// DELETE /admin/tenants/:id — Delete a tenant @@ -149,7 +151,8 @@ pub async fn delete_tenant( State(state): State, Path(id): Path, ) -> AppResult> { - state.tenant.delete(&id).await?; + let id = crate::utils::id::parse_id(&id)?; + state.tenant.delete(id).await?; Ok(ApiResponse::success(serde_json::json!({ "deleted": true, }))) @@ -166,7 +169,10 @@ pub async fn admin_batch( ) -> AppResult> { crate::errors::validation::validate(&req)?; let mut affected = 0usize; - for id in &req.ids { + for raw_id in &req.ids { + let Ok(id) = crate::utils::id::parse_id(raw_id) else { + continue; + }; match req.action.as_str() { "delete" => { if state.tenant.delete(id).await.is_ok() { diff --git a/src/handlers/user.rs b/src/handlers/user.rs index c140f2c9..daad96fb 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -190,7 +190,8 @@ pub async fn get_user( State(state): State, Path(id): Path, ) -> AppResult> { - let u = user::get_public_user(&state.pool, &id, auth.tenant_id()).await?; + let id = crate::utils::id::parse_id(&id)?; + let u = user::get_public_user(&state.pool, id, auth.tenant_id()).await?; Ok(ApiResponse::success(u)) } @@ -253,7 +254,8 @@ pub async fn admin_get_user( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - let u = user::get_public_user(&state.pool, &id, auth.tenant_id()).await?; + let id = crate::utils::id::parse_id(&id)?; + let u = user::get_public_user(&state.pool, id, auth.tenant_id()).await?; Ok(ApiResponse::success(u)) } diff --git a/src/handlers/user_address.rs b/src/handlers/user_address.rs index e7b4f141..2bda00fd 100644 --- a/src/handlers/user_address.rs +++ b/src/handlers/user_address.rs @@ -9,7 +9,7 @@ use axum::Json; use axum::extract::{Path, State}; fn resolve_user_id(auth: &AuthUser) -> AppResult { - auth.user_int_id() + auth.user_id() .ok_or_else(|| crate::errors::app_error::AppError::Unauthorized) } @@ -93,9 +93,10 @@ pub async fn update_address( ) -> AppResult> { let user_id = resolve_user_id(&auth)?; validation::validate(&req)?; + let id = crate::utils::id::parse_id(&id)?; let addr = state .user_address_service - .update(&auth, user_id, &id, req) + .update(&auth, user_id, id, req) .await?; Ok(ApiResponse::success(UserAddressResponse::from(addr))) } @@ -106,9 +107,10 @@ pub async fn delete_address( Path(id): Path, ) -> AppResult> { let user_id = resolve_user_id(&auth)?; + let id = crate::utils::id::parse_id(&id)?; state .user_address_service - .delete(&auth, user_id, &id) + .delete(&auth, user_id, id) .await?; Ok(ApiResponse::success(())) } diff --git a/src/handlers/wallet.rs b/src/handlers/wallet.rs index ab3fa891..1270da6e 100644 --- a/src/handlers/wallet.rs +++ b/src/handlers/wallet.rs @@ -119,7 +119,7 @@ pub fn routes( r, registry, restful, - "/admin/wallets/{tx_doc_id}/reversal", + "/admin/wallets/{tx_id}/reversal", post, admin_reversal, "admin wallet", @@ -136,9 +136,10 @@ pub async fn list_wallets( State(state): State, ) -> Result>, AppError> { let user_id = auth.ensure_authenticated()?; + let user_id_str = user_id.to_string(); let wallets = state .wallet_service - .list_wallets_by_user(user_id, auth.tenant_id()) + .list_wallets_by_user(&user_id_str, auth.tenant_id()) .await?; let items: Vec = wallets .into_iter() @@ -158,9 +159,10 @@ pub async fn get_wallet( Path(currency): Path, ) -> Result, AppError> { let user_id = auth.ensure_authenticated()?; + let user_id_str = user_id.to_string(); let w = state .wallet_service - .get_wallet_by_currency(user_id, ¤cy, auth.tenant_id()) + .get_wallet_by_currency(&user_id_str, ¤cy, auth.tenant_id()) .await?; Ok(ApiResponse::success(dto::WalletResponse::from_wallet(w)?)) } @@ -180,10 +182,11 @@ pub async fn list_transactions( AppError, > { let user_id = auth.ensure_authenticated()?; + let user_id_str = user_id.to_string(); let (rows, total) = state .wallet_service .list_transactions_by_wallet( - user_id, + &user_id_str, ¤cy, params.page, params.page_size, @@ -207,9 +210,15 @@ pub async fn list_all_transactions( AppError, > { let user_id = auth.ensure_authenticated()?; + let user_id_str = user_id.to_string(); let (rows, total) = state .wallet_service - .list_transactions_by_user(user_id, params.page, params.page_size, auth.tenant_id()) + .list_transactions_by_user( + &user_id_str, + params.page, + params.page_size, + auth.tenant_id(), + ) .await?; let items = state.wallet_service.tx_list_to_response(rows).await?; Ok(params.paginate(items, total)) @@ -335,7 +344,7 @@ pub async fn admin_debit( pub async fn list_user_transactions( auth: AuthUser, State(state): State, - Path((user_doc_id, currency)): Path<(String, String)>, + Path((user_id, currency)): Path<(String, String)>, Query(params): Query, ) -> Result< ApiResponse>, @@ -345,7 +354,7 @@ pub async fn list_user_transactions( let (rows, total) = state .wallet_service .list_transactions_by_wallet( - &user_doc_id, + &user_id, ¤cy, params.page, params.page_size, @@ -365,7 +374,7 @@ pub async fn list_user_transactions( pub async fn list_user_all_transactions( auth: AuthUser, State(state): State, - Path(user_doc_id): Path, + Path(user_id): Path, Query(params): Query, ) -> Result< ApiResponse>, @@ -374,28 +383,23 @@ pub async fn list_user_all_transactions( auth.ensure_admin()?; let (rows, total) = state .wallet_service - .list_transactions_by_user( - &user_doc_id, - params.page, - params.page_size, - auth.tenant_id(), - ) + .list_transactions_by_user(&user_id, params.page, params.page_size, auth.tenant_id()) .await?; let items = state.wallet_service.tx_list_to_response(rows).await?; Ok(params.paginate(items, total)) } -#[utoipa::path(post, path = "/admin/wallets/{tx_doc_id}/reversal", tag = "wallets", +#[utoipa::path(post, path = "/admin/wallets/{tx_id}/reversal", tag = "wallets", security(("bearer_auth" = [])), - params(("tx_doc_id" = String, Path, description = "Transaction document ID")), + params(("tx_id" = String, Path, description = "Transaction ID")), request_body = dto::ReversalRequest, responses((status = 200, description = "Transaction reversed")) )] pub async fn admin_reversal( auth: AuthUser, State(state): State, - Path(tx_doc_id): Path, + Path(tx_id): Path, Json(req): Json, ) -> Result, AppError> { auth.ensure_admin()?; @@ -403,12 +407,12 @@ pub async fn admin_reversal( let original = state .wallet_service - .find_tx_by_document_id(&tx_doc_id, auth.tenant_id()) + .find_tx_by_id(&tx_id, auth.tenant_id()) .await?; let tx = state .wallet_service - .reverse_transaction(original.id, &req.transaction_no) + .reverse_transaction(*original.id, &req.transaction_no) .await?; let resp = state.wallet_service.tx_to_response(tx).await?; diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs index 39b53aae..3305f19b 100644 --- a/src/middleware/auth.rs +++ b/src/middleware/auth.rs @@ -42,16 +42,14 @@ use crate::errors::app_error::{AppError, AppResult}; use crate::models::user::UserRole; struct Claims { - user_id: String, - user_int_id: i64, + user_id: i64, role: UserRole, tenant_id: String, } #[derive(Debug, Clone)] struct RequestIdentity { - user_id: Option, - user_int_id: Option, + user_id: Option, role: UserRole, tenant_id: Option, is_super_admin: bool, @@ -65,12 +63,8 @@ struct RequestIdentity { pub struct AuthUser(RequestIdentity); impl AuthUser { - pub fn user_id(&self) -> Option<&str> { - self.0.user_id.as_deref() - } - - pub fn user_int_id(&self) -> Option { - self.0.user_int_id + pub fn user_id(&self) -> Option { + self.0.user_id } pub fn role(&self) -> &str { @@ -97,8 +91,8 @@ impl AuthUser { self.0.is_super_admin } - pub fn ensure_authenticated(&self) -> AppResult<&str> { - self.0.user_id.as_deref().ok_or(AppError::Unauthorized) + pub fn ensure_authenticated(&self) -> AppResult { + self.0.user_id.ok_or(AppError::Unauthorized) } pub fn ensure_admin(&self) -> AppResult<()> { @@ -117,15 +111,9 @@ impl AuthUser { } } - pub fn from_parts( - user_id: Option, - user_int_id: Option, - role: UserRole, - tenant_id: Option, - ) -> Self { + pub fn from_parts(user_id: Option, role: UserRole, tenant_id: Option) -> Self { AuthUser(RequestIdentity { user_id, - user_int_id, role, tenant_id, is_super_admin: false, @@ -135,14 +123,10 @@ impl AuthUser { #[cfg(test)] impl AuthUser { - pub fn new_test(user_id: &str, role: UserRole, tenant_id: &str) -> Self { + pub fn new_test(user_id: i64, role: UserRole, tenant_id: &str) -> Self { + let uid = if user_id == 0 { None } else { Some(user_id) }; AuthUser(RequestIdentity { - user_id: if user_id.is_empty() { - None - } else { - Some(user_id.to_string()) - }, - user_int_id: None, + user_id: uid, role, tenant_id: if tenant_id.is_empty() { None @@ -153,14 +137,10 @@ impl AuthUser { }) } - pub fn new_test_super_admin(user_id: &str, tenant_id: &str) -> Self { + pub fn new_test_super_admin(user_id: i64, tenant_id: &str) -> Self { + let uid = if user_id == 0 { None } else { Some(user_id) }; AuthUser(RequestIdentity { - user_id: if user_id.is_empty() { - None - } else { - Some(user_id.to_string()) - }, - user_int_id: None, + user_id: uid, role: UserRole::Admin, tenant_id: if tenant_id.is_empty() { None @@ -193,22 +173,20 @@ async fn extract_claims(parts: &Parts, state: &AppState) -> Option { let token = extract_bearer_token(parts)?; if crate::services::api_token::is_api_token(token) { - let (user_id, user_int_id, role, tenant_id) = + let (user_id, role, tenant_id) = crate::services::api_token::verify_api_token(&state.pool, &*state.cache, token) .await .ok()?; let role: UserRole = role.parse().ok()?; Some(Claims { user_id, - user_int_id, role, tenant_id: tenant_id.unwrap_or_else(|| crate::constants::DEFAULT_TENANT.to_string()), }) } else { let claims = crate::services::auth::verify_token(token, &state.jwt_decoding_key).ok()?; Some(Claims { - user_id: claims.sub, - user_int_id: claims.iid, + user_id: claims.sub.parse().ok()?, role: claims.role, tenant_id: claims.tenant_id, }) @@ -232,35 +210,30 @@ impl FromRequestParts for AuthUser { let identity = match (claims, header_tenant) { (Some(c), Some(ht)) if c.role == UserRole::Admin => RequestIdentity { user_id: Some(c.user_id), - user_int_id: Some(c.user_int_id), role: c.role, tenant_id: if no_tenant { None } else { Some(ht) }, is_super_admin: true, }, (Some(c), None) if c.role == UserRole::Admin => RequestIdentity { user_id: Some(c.user_id), - user_int_id: Some(c.user_int_id), role: c.role, tenant_id: None, is_super_admin: true, }, (Some(c), _) => RequestIdentity { user_id: Some(c.user_id), - user_int_id: Some(c.user_int_id), role: c.role, tenant_id: if no_tenant { None } else { Some(c.tenant_id) }, is_super_admin: false, }, (None, Some(ht)) => RequestIdentity { user_id: None, - user_int_id: None, role: UserRole::Reader, tenant_id: if no_tenant { None } else { Some(ht) }, is_super_admin: false, }, (None, None) => RequestIdentity { user_id: None, - user_int_id: None, role: UserRole::Reader, tenant_id: if no_tenant { None @@ -283,14 +256,8 @@ mod tests { #[test] fn from_parts_all_fields_accessors() { - let auth = AuthUser::from_parts( - Some("uid-123".to_string()), - Some(42), - UserRole::Author, - Some("tenant-1".to_string()), - ); - assert_eq!(auth.user_id(), Some("uid-123")); - assert_eq!(auth.user_int_id(), Some(42)); + let auth = AuthUser::from_parts(Some(42), UserRole::Author, Some("tenant-1".to_string())); + assert_eq!(auth.user_id(), Some(42)); assert_eq!(auth.role(), "author"); assert_eq!(auth.tenant_id(), Some("tenant-1")); assert!(auth.is_authenticated()); @@ -298,22 +265,16 @@ mod tests { #[test] fn from_parts_no_user_id_not_authenticated() { - let auth = AuthUser::from_parts(None, None, UserRole::Reader, Some("t1".to_string())); + let auth = AuthUser::from_parts(None, UserRole::Reader, Some("t1".to_string())); assert!(!auth.is_authenticated()); assert!(auth.user_id().is_none()); - assert!(auth.user_int_id().is_none()); let err = auth.ensure_authenticated().unwrap_err(); assert!(matches!(err, AppError::Unauthorized)); } #[test] fn admin_role_passes_admin_checks() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Admin, - Some("t1".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Admin, Some("t1".to_string())); assert!(auth.is_admin()); assert!(auth.ensure_admin().is_ok()); assert!(auth.is_author()); @@ -322,12 +283,7 @@ mod tests { #[test] fn reader_role_denied_admin_and_author() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Reader, - Some("t1".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Reader, Some("t1".to_string())); assert!(!auth.is_admin()); assert!(matches!( auth.ensure_admin().unwrap_err(), @@ -342,12 +298,7 @@ mod tests { #[test] fn author_role_passes_author_checks() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Author, - Some("t1".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Author, Some("t1".to_string())); assert!(auth.is_author()); assert!(auth.ensure_author().is_ok()); assert!(!auth.is_admin()); @@ -359,7 +310,7 @@ mod tests { #[test] fn super_admin_flag_true() { - let auth = AuthUser::new_test_super_admin("uid", "t1"); + let auth = AuthUser::new_test_super_admin(1, "t1"); assert!(auth.is_super_admin()); assert!(auth.is_admin()); assert!(auth.is_authenticated()); @@ -367,35 +318,25 @@ mod tests { #[test] fn from_parts_super_admin_flag_false() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Admin, - Some("t1".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Admin, Some("t1".to_string())); assert!(!auth.is_super_admin()); } #[test] fn tenant_id_some() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Reader, - Some("my-tenant".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Reader, Some("my-tenant".to_string())); assert_eq!(auth.tenant_id(), Some("my-tenant")); } #[test] fn tenant_id_none() { - let auth = AuthUser::from_parts(Some("uid".to_string()), Some(1), UserRole::Reader, None); + let auth = AuthUser::from_parts(Some(1), UserRole::Reader, None); assert!(auth.tenant_id().is_none()); } #[test] fn unauthenticated_ensure_admin_and_author_forbidden() { - let auth = AuthUser::from_parts(None, None, UserRole::Reader, None); + let auth = AuthUser::from_parts(None, UserRole::Reader, None); assert!(matches!( auth.ensure_admin().unwrap_err(), AppError::Forbidden @@ -407,8 +348,8 @@ mod tests { } #[test] - fn new_test_with_empty_strings_is_anonymous() { - let auth = AuthUser::new_test("", UserRole::Reader, ""); + fn new_test_with_zero_id_is_anonymous() { + let auth = AuthUser::new_test(0, UserRole::Reader, ""); assert!(!auth.is_authenticated()); assert!(auth.user_id().is_none()); assert!(auth.tenant_id().is_none()); @@ -416,12 +357,7 @@ mod tests { #[test] fn editor_role_not_admin_not_author() { - let auth = AuthUser::from_parts( - Some("uid".to_string()), - Some(1), - UserRole::Editor, - Some("t1".to_string()), - ); + let auth = AuthUser::from_parts(Some(1), UserRole::Editor, Some("t1".to_string())); assert!(!auth.is_admin()); assert!(!auth.is_author()); } diff --git a/src/models/api_token.rs b/src/models/api_token.rs index f576e925..a6ee8023 100644 --- a/src/models/api_token.rs +++ b/src/models/api_token.rs @@ -11,14 +11,14 @@ use ts_rs::TS; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// API Token full database row model #[derive(Debug, FromRow, Serialize, Deserialize)] pub struct ApiToken { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub name: String, pub token_hash: String, pub token_prefix: String, @@ -32,8 +32,7 @@ pub struct ApiToken { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, FromRow, Serialize, Deserialize)] pub struct ApiTokenListItem { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub name: String, pub token_prefix: String, pub scopes: String, @@ -52,9 +51,9 @@ pub async fn create( scopes: &str, expires_at: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "api_tokens", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "name" => name, "token_hash" => token_hash, @@ -63,7 +62,7 @@ pub async fn create( "expires_at" => expires_at, "created_at" => now ])?; - find_by_id(pool, &document_id).await?.ok_or_else(|| { + find_by_id(pool, id).await?.ok_or_else(|| { crate::errors::app_error::AppError::Internal(anyhow::anyhow!( "failed to fetch newly created api token" )) @@ -84,7 +83,6 @@ pub async fn list_by_user( raisfast_derive::check_schema!( "api_tokens", "id", - "document_id", "name", "token_prefix", "scopes", @@ -94,7 +92,7 @@ pub async fn list_by_user( "user_id" ); let sql = format!( - "SELECT id, document_id, name, token_prefix, scopes, last_used_at, expires_at, created_at FROM api_tokens WHERE user_id = {} ORDER BY created_at DESC", + "SELECT id, name, token_prefix, scopes, last_used_at, expires_at, created_at FROM api_tokens WHERE user_id = {} ORDER BY created_at DESC", ph(1) ); let rows = sqlx::query_as::<_, ApiTokenListItem>(&sql) @@ -104,15 +102,14 @@ pub async fn list_by_user( Ok(rows) } -/// Find API Token by document_id -pub async fn find_by_id(pool: &crate::db::Pool, document_id: &str) -> AppResult> { - raisfast_derive::crud_find!(pool, "api_tokens", ApiToken, "document_id" => document_id) - .map_err(Into::into) +/// Find API Token by id +pub async fn find_by_id(pool: &crate::db::Pool, id: i64) -> AppResult> { + raisfast_derive::crud_find!(pool, "api_tokens", ApiToken, "id" => id).map_err(Into::into) } -/// Delete API Token by document_id -pub async fn delete_by_id(pool: &crate::db::Pool, document_id: &str) -> AppResult<()> { - raisfast_derive::crud_delete!(pool, "api_tokens", "document_id" => document_id)?; +/// Delete API Token by id +pub async fn delete_by_id(pool: &crate::db::Pool, id: i64) -> AppResult<()> { + raisfast_derive::crud_delete!(pool, "api_tokens", "id" => id)?; Ok(()) } @@ -138,14 +135,14 @@ mod tests { let user = crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - user.id + *user.id } #[tokio::test] @@ -183,7 +180,7 @@ mod tests { #[tokio::test] async fn find_by_id_not_found() { let pool = setup_pool().await; - let result = find_by_id(&pool, "nonexistent-doc-id").await.unwrap(); + let result = find_by_id(&pool, 99999).await.unwrap(); assert!(result.is_none()); } @@ -227,8 +224,8 @@ mod tests { let row = create(&pool, user_id, "Del", "h3", "rblog_c", "[\"read\"]", None) .await .unwrap(); - delete_by_id(&pool, &row.document_id).await.unwrap(); - let found = find_by_id(&pool, &row.document_id).await.unwrap(); + delete_by_id(&pool, *row.id).await.unwrap(); + let found = find_by_id(&pool, *row.id).await.unwrap(); assert!(found.is_none()); } @@ -241,8 +238,8 @@ mod tests { .unwrap(); assert!(row.last_used_at.is_none()); - touch_last_used(&pool, row.id).await.unwrap(); - let updated = find_by_id(&pool, &row.document_id).await.unwrap().unwrap(); + touch_last_used(&pool, *row.id).await.unwrap(); + let updated = find_by_id(&pool, *row.id).await.unwrap().unwrap(); assert!(updated.last_used_at.is_some()); } diff --git a/src/models/audit_log.rs b/src/models/audit_log.rs index 72eaf52a..264ff707 100644 --- a/src/models/audit_log.rs +++ b/src/models/audit_log.rs @@ -5,16 +5,16 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// Full database row for an audit log entry #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct AuditEntry { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub actor_id: Option, + pub actor_id: Option, pub actor_role: Option, pub action: String, pub subject: String, @@ -31,7 +31,7 @@ pub async fn insert(pool: &crate::db::Pool, entry: &AuditEntry) -> AppResult<()> pool, "audit_log", [ - "document_id" => &entry.document_id, + "id" => entry.id, "actor_id" => entry.actor_id, "actor_role" => &entry.actor_role, "action" => &entry.action, @@ -73,11 +73,3 @@ pub async fn find_paginated( pub async fn find_by_id(pool: &crate::db::Pool, id: i64) -> AppResult { raisfast_derive::crud_find_one!(pool, "audit_log", AuditEntry, "id" => id).map_err(Into::into) } - -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, -) -> AppResult { - raisfast_derive::crud_find_one!(pool, "audit_log", AuditEntry, "document_id" => document_id) - .map_err(Into::into) -} diff --git a/src/models/cart_item.rs b/src/models/cart_item.rs index 0c7b9669..03f5bc01 100644 --- a/src/models/cart_item.rs +++ b/src/models/cart_item.rs @@ -3,17 +3,17 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct CartItem { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub user_id: i64, - pub product_id: i64, - pub variant_id: Option, + pub user_id: SnowflakeId, + pub product_id: SnowflakeId, + pub variant_id: Option, pub quantity: i64, pub attributes: Option, pub created_at: Timestamp, @@ -66,24 +66,6 @@ pub async fn find_by_user_and_product( } } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult { - raisfast_derive::crud_find_one!(pool, "cart_items", CartItem, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - -pub async fn find_by_document_id_opt( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "cart_items", CartItem, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn insert( pool: &crate::db::Pool, user_id: i64, @@ -93,13 +75,12 @@ pub async fn insert( attributes: Option<&str>, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "cart_items", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "product_id" => product_id, "variant_id" => variant_id, @@ -110,7 +91,8 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id).await + raisfast_derive::crud_find_one!(pool, "cart_items", CartItem, "id" => id, tenant: tenant_id) + .map_err(Into::into) } pub async fn update_quantity( @@ -130,12 +112,20 @@ pub async fn update_quantity( AppError::expect_affected(&result, "cart_item") } -pub async fn delete_by_document_id( +pub async fn find_by_id( pool: &crate::db::Pool, - document_id: &str, + id: i64, + tenant_id: Option<&str>, +) -> AppResult> { + Ok(raisfast_derive::crud_find!(pool, "cart_items", CartItem, "id" => id, tenant: tenant_id)?) +} + +pub async fn delete_by_id( + pool: &crate::db::Pool, + id: i64, tenant_id: Option<&str>, ) -> AppResult<()> { - let result = raisfast_derive::crud_delete!(pool, "cart_items", "document_id" => document_id, tenant: tenant_id)?; + let result = raisfast_derive::crud_delete!(pool, "cart_items", "id" => id, tenant: tenant_id)?; AppError::expect_affected(&result, "cart_item") } @@ -175,21 +165,16 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", ) - .bind(&doc_id) + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -237,7 +222,7 @@ mod tests { } #[tokio::test] - async fn insert_and_find_by_document_id() { + async fn insert_and_find() { let pool = setup_pool().await; let uid = seed_user(&pool).await; let pid = seed_product(&pool).await; @@ -250,7 +235,7 @@ mod tests { assert_eq!(item.product_id, pid); assert_eq!(item.quantity, 2); assert_eq!(item.attributes.as_deref(), Some(r#"{"color":"red"}"#)); - assert!(!item.document_id.is_empty()); + assert!(*item.id > 0); } #[tokio::test] @@ -308,7 +293,7 @@ mod tests { } #[tokio::test] - async fn find_by_document_id_opt() { + async fn find_by_id_opt() { let pool = setup_pool().await; let uid = seed_user(&pool).await; let pid = seed_product(&pool).await; @@ -317,13 +302,13 @@ mod tests { .await .unwrap(); - let found = super::find_by_document_id_opt(&pool, &item.document_id, None) + let found = super::find_by_user_and_product(&pool, uid, pid, None, None) .await .unwrap(); assert!(found.is_some()); assert_eq!(found.unwrap().id, item.id); - let not_found = super::find_by_document_id_opt(&pool, "nonexistent", None) + let not_found = super::find_by_user_and_product(&pool, uid, 99999, None, None) .await .unwrap(); assert!(not_found.is_none()); @@ -339,12 +324,13 @@ mod tests { .await .unwrap(); - super::update_quantity(&pool, item.id, 10, None) + super::update_quantity(&pool, *item.id, 10, None) .await .unwrap(); - let updated = super::find_by_document_id(&pool, &item.document_id, None) + let updated = super::find_by_user_and_product(&pool, uid, pid, None, None) .await + .unwrap() .unwrap(); assert_eq!(updated.quantity, 10); } @@ -357,7 +343,7 @@ mod tests { } #[tokio::test] - async fn delete_by_document_id() { + async fn delete_by_id() { let pool = setup_pool().await; let uid = seed_user(&pool).await; let pid = seed_product(&pool).await; @@ -366,20 +352,18 @@ mod tests { .await .unwrap(); - super::delete_by_document_id(&pool, &item.document_id, None) - .await - .unwrap(); + super::delete_by_id(&pool, *item.id, None).await.unwrap(); - let found = super::find_by_document_id_opt(&pool, &item.document_id, None) + let found = super::find_by_user_and_product(&pool, uid, pid, None, None) .await .unwrap(); assert!(found.is_none()); } #[tokio::test] - async fn delete_by_document_id_nonexistent() { + async fn delete_by_id_nonexistent() { let pool = setup_pool().await; - let result = super::delete_by_document_id(&pool, "nonexistent", None).await; + let result = super::delete_by_id(&pool, 99999, None).await; assert!(result.is_err()); } diff --git a/src/models/category.rs b/src/models/category.rs index 67288048..eb468eb2 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -9,13 +9,13 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Category { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub name: String, pub slug: String, @@ -26,10 +26,10 @@ pub struct Category { pub og_title: Option, pub og_description: Option, pub og_image: Option, - pub parent_id: Option, + pub parent_id: Option, pub sort_order: i64, - pub created_by: Option, - pub updated_by: Option, + pub created_by: Option, + pub updated_by: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -65,28 +65,19 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "categories", Category, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn create( pool: &crate::db::Pool, cmd: &crate::commands::CreateCategoryCmd, tenant_id: Option<&str>, created_by: Option, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "categories", [ - "document_id" => &document_id, + "id" => id, "name" => &cmd.name, "slug" => &cmd.slug, "description" => &cmd.description, @@ -100,9 +91,9 @@ pub async fn create( tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch created category"))) + find_by_id(pool, id, tenant_id) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("failed to fetch created category: {e}"))) } pub async fn update( @@ -121,7 +112,7 @@ pub async fn update( .as_deref() .map(std::string::ToString::to_string) .or(existing.description); - let parent = cmd.parent_id.or(existing.parent_id); + let parent = cmd.parent_id.or(existing.parent_id.map(|v| *v)); let sort = cmd.sort_order.unwrap_or(existing.sort_order); let now = crate::utils::tz::now_utc(); @@ -163,24 +154,11 @@ mod tests { async fn create_and_find_by_id() { let pool = setup_pool().await; let cat = create(&pool, &make_cmd("Tech"), None, None).await.unwrap(); - let found = find_by_id(&pool, cat.id, None).await.unwrap(); + let found = find_by_id(&pool, *cat.id, None).await.unwrap(); assert_eq!(found.id, cat.id); assert_eq!(found.name, "Tech"); } - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let cat = create(&pool, &make_cmd("Science"), None, None) - .await - .unwrap(); - let found = super::find_by_document_id(&pool, &cat.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, cat.id); - } - #[tokio::test] async fn find_all_returns_all() { let pool = setup_pool().await; @@ -209,7 +187,7 @@ mod tests { let updated = update( &pool, &UpdateCategoryCmd { - id: cat.id, + id: *cat.id, name: Some("New".to_string()), slug: None, description: None, @@ -228,8 +206,8 @@ mod tests { async fn delete_removes_category() { let pool = setup_pool().await; let cat = create(&pool, &make_cmd("Gone"), None, None).await.unwrap(); - delete(&pool, cat.id, None).await.unwrap(); - let result = find_by_id(&pool, cat.id, None).await; + delete(&pool, *cat.id, None).await.unwrap(); + let result = find_by_id(&pool, *cat.id, None).await; assert!(result.is_err()); } diff --git a/src/models/comment.rs b/src/models/comment.rs index ecc8e477..420ce56b 100644 --- a/src/models/comment.rs +++ b/src/models/comment.rs @@ -14,6 +14,7 @@ use sqlx::FromRow; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -27,16 +28,15 @@ define_enum!( #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] #[non_exhaustive] pub struct Comment { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub post_id: i64, - pub created_by: Option, - pub updated_by: Option, + pub post_id: SnowflakeId, + pub created_by: Option, + pub updated_by: Option, pub nickname: Option, pub email: Option, pub content: String, - pub parent_id: Option, + pub parent_id: Option, pub author_ip: Option, pub author_url: Option, pub status: CommentStatus, @@ -48,16 +48,15 @@ pub struct Comment { #[derive(Debug, Serialize, Clone)] #[non_exhaustive] pub struct CommentResponse { - pub id: i64, - pub document_id: String, - pub post_id: i64, - pub created_by: Option, + pub id: String, pub nickname: Option, pub content: String, - pub parent_id: Option, pub depth: i32, pub replies: Vec, pub created_at: Timestamp, + pub post_id: Option, + pub created_by: Option, + pub parent_id: Option, } pub async fn find_by_id( @@ -68,28 +67,18 @@ pub async fn find_by_id( Ok(raisfast_derive::crud_find!(pool, "comments", Comment, "id" => id, tenant: tenant_id)?) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - Ok( - raisfast_derive::crud_find!(pool, "comments", Comment, "document_id" => document_id, tenant: tenant_id)?, - ) -} - pub async fn create( pool: &crate::db::Pool, cmd: &crate::commands::CreateCommentCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "comments", [ - "document_id" => &document_id, + "id" => id, "post_id" => cmd.post_id, "created_by" => cmd.created_by, "updated_by" => cmd.created_by, @@ -104,7 +93,7 @@ pub async fn create( tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) + find_by_id(pool, id, tenant_id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch created comment"))) } @@ -147,20 +136,46 @@ pub async fn find_all_by_post( } #[cfg_attr(feature = "export-types", derive(TS))] -#[derive(Debug, FromRow, Serialize, Clone)] +#[derive(Debug, Serialize, Clone)] pub struct AdminCommentRow { - pub id: i64, - pub post_id: i64, + pub id: String, pub post_title: String, - pub created_by: Option, pub nickname: Option, pub email: Option, pub content: String, - pub parent_id: Option, pub status: CommentStatus, pub created_at: Timestamp, } +#[allow(dead_code)] +#[derive(Debug, FromRow)] +struct AdminCommentRowDb { + id: SnowflakeId, + post_id: SnowflakeId, + post_title: String, + created_by: Option, + nickname: Option, + email: Option, + content: String, + parent_id: Option, + status: CommentStatus, + created_at: Timestamp, +} + +impl From for AdminCommentRow { + fn from(r: AdminCommentRowDb) -> Self { + Self { + id: r.id.to_string(), + post_title: r.post_title, + nickname: r.nickname, + email: r.email, + content: r.content, + status: r.status, + created_at: r.created_at, + } + } +} + pub async fn find_all_paginated( pool: &crate::db::Pool, page: i64, @@ -168,7 +183,7 @@ pub async fn find_all_paginated( tenant_id: Option<&str>, ) -> AppResult<(Vec, i64)> { let result = raisfast_derive::crud_query_paged!( - pool, AdminCommentRow, + pool, AdminCommentRowDb, data_sql: "SELECT c.id, c.post_id, p.title AS post_title, c.created_by, c.nickname, c.email, c.content, c.parent_id, c.status, c.created_at FROM comments c JOIN posts p ON c.post_id = p.id WHERE 1=1{tenant} ORDER BY c.created_at DESC", count_sql: "SELECT COUNT(*) FROM comments WHERE 1=1{tenant}", binds: [], @@ -176,7 +191,8 @@ pub async fn find_all_paginated( page: page, page_size: page_size ); - Ok(result) + let (rows, total) = result; + Ok((rows.into_iter().map(AdminCommentRow::from).collect(), total)) } pub async fn update_status( @@ -220,18 +236,19 @@ fn get_depth(comments: &[Comment], comment: &Comment) -> i32 { #[must_use] pub fn build_tree(comments: &[Comment]) -> Vec { - let map: std::collections::HashMap> = + let root_key = SnowflakeId(0); + let map: std::collections::HashMap> = comments .iter() .fold(std::collections::HashMap::new(), |mut acc, c| { - let key = c.parent_id.unwrap_or_default(); + let key = c.parent_id.unwrap_or(root_key); acc.entry(key).or_default().push(c.clone()); acc }); fn build( - parent_id: i64, - map: &std::collections::HashMap>, + parent_id: SnowflakeId, + map: &std::collections::HashMap>, comments: &[Comment], ) -> Vec { map.get(&parent_id) @@ -242,16 +259,15 @@ pub fn build_tree(comments: &[Comment]) -> Vec { let depth = get_depth(comments, c); let replies = build(c.id, map, comments); CommentResponse { - id: c.id, - document_id: c.document_id.clone(), - post_id: c.post_id, - created_by: c.created_by, + id: c.id.to_string(), nickname: c.nickname.clone(), content: c.content.clone(), - parent_id: c.parent_id, depth, replies, created_at: c.created_at, + post_id: None, + created_by: None, + parent_id: None, } }) .collect() @@ -259,7 +275,7 @@ pub fn build_tree(comments: &[Comment]) -> Vec { .unwrap_or_default() } - build(0, &map, comments) + build(root_key, &map, comments) } const MAX_DEPTH: i32 = 3; @@ -283,16 +299,15 @@ mod tests { fn make_comment(id: i64, post_id: i64, parent_id: Option) -> Comment { Comment { - id, + id: crate::utils::id::SnowflakeId(id), tenant_id: Some(crate::constants::DEFAULT_TENANT.to_string()), - document_id: format!("doc-{id}"), - post_id, + post_id: crate::utils::id::SnowflakeId(post_id), created_by: None, updated_by: None, nickname: None, email: None, content: "test".to_string(), - parent_id, + parent_id: parent_id.map(crate::utils::id::SnowflakeId), author_ip: None, author_url: None, status: CommentStatus::Approved, @@ -319,11 +334,11 @@ mod tests { ]; let tree = build_tree(&comments); assert_eq!(tree.len(), 1); - assert_eq!(tree[0].id, 1); + assert_eq!(tree[0].id, "1"); assert_eq!(tree[0].replies.len(), 1); - assert_eq!(tree[0].replies[0].id, 2); + assert_eq!(tree[0].replies[0].id, "2"); assert_eq!(tree[0].replies[0].replies.len(), 1); - assert_eq!(tree[0].replies[0].replies[0].id, 3); + assert_eq!(tree[0].replies[0].replies[0].id, "3"); } #[test] @@ -381,29 +396,23 @@ mod tests { ) .await .unwrap(); - user.id + *user.id } async fn insert_post(pool: &crate::db::Pool, user_id: i64) -> i64 { - let doc_id = crate::utils::id::new_document_id(); - let slug = format!("slug-{doc_id}"); + let post_id = crate::utils::id::new_id(); + let slug = format!("slug-{post_id}"); sqlx::query( - "INSERT INTO posts (document_id, title, slug, content, status, created_by, updated_by) VALUES (?, 'Test', ?, 'content', 'published', ?, ?)", + "INSERT INTO posts (id, title, slug, content, status, created_by, updated_by) VALUES (?, 'Test', ?, 'content', 'published', ?, ?)", ) - .bind(&doc_id) + .bind(post_id) .bind(&slug) .bind(user_id) .bind(user_id) .execute(pool) .await .unwrap(); - - let (id,): (i64,) = sqlx::query_as("SELECT id FROM posts WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); - id + post_id } fn make_cmd(post_id: i64) -> CreateCommentCmd { @@ -425,23 +434,18 @@ mod tests { let c = create(&pool, &make_cmd(pid), None).await.unwrap(); assert_eq!(c.post_id, pid); assert_eq!(c.content, "hello"); - let found = super::find_by_id(&pool, c.id, None).await.unwrap().unwrap(); - assert_eq!(found.id, c.id); - assert_eq!(found.document_id, c.document_id); - } - - #[tokio::test] - async fn find_by_document_id_test() { - let pool = setup_pool().await; - let uid = insert_user(&pool).await; - let pid = insert_post(&pool, uid).await; - let c = create(&pool, &make_cmd(pid), None).await.unwrap(); - let found = super::find_by_document_id(&pool, &c.document_id, None) + let found = super::find_by_id(&pool, *c.id, None) .await .unwrap() .unwrap(); assert_eq!(found.id, c.id); - assert_eq!(found.content, "hello"); + } + + #[tokio::test] + async fn find_by_id_not_found() { + let pool = setup_pool().await; + let result = super::find_by_id(&pool, 99999, None).await.unwrap(); + assert!(result.is_none()); } #[tokio::test] @@ -451,7 +455,7 @@ mod tests { let pid = insert_post(&pool, uid).await; let c1 = create(&pool, &make_cmd(pid), None).await.unwrap(); let _c2 = create(&pool, &make_cmd(pid), None).await.unwrap(); - update_status(&pool, c1.id, CommentStatus::Approved, None) + update_status(&pool, *c1.id, CommentStatus::Approved, None) .await .unwrap(); @@ -472,7 +476,7 @@ mod tests { let mut cmd = make_cmd(pid); cmd.content = format!("comment {i}"); let c = create(&pool, &cmd, None).await.unwrap(); - update_status(&pool, c.id, CommentStatus::Approved, None) + update_status(&pool, *c.id, CommentStatus::Approved, None) .await .unwrap(); ids.push(c.id); @@ -497,10 +501,13 @@ mod tests { let pid = insert_post(&pool, uid).await; let c = create(&pool, &make_cmd(pid), None).await.unwrap(); assert_eq!(c.status, CommentStatus::Pending); - update_status(&pool, c.id, CommentStatus::Approved, None) + update_status(&pool, *c.id, CommentStatus::Approved, None) .await .unwrap(); - let found = super::find_by_id(&pool, c.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *c.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.status, CommentStatus::Approved); } @@ -510,8 +517,8 @@ mod tests { let uid = insert_user(&pool).await; let pid = insert_post(&pool, uid).await; let c = create(&pool, &make_cmd(pid), None).await.unwrap(); - super::delete(&pool, c.id, None).await.unwrap(); - let found = super::find_by_id(&pool, c.id, None).await.unwrap(); + super::delete(&pool, *c.id, None).await.unwrap(); + let found = super::find_by_id(&pool, *c.id, None).await.unwrap(); assert!(found.is_none()); } diff --git a/src/models/content_revision.rs b/src/models/content_revision.rs index 8ee38d7e..f6dd49e1 100644 --- a/src/models/content_revision.rs +++ b/src/models/content_revision.rs @@ -12,39 +12,38 @@ use ts_rs::TS; use crate::db::dialect::ph; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, FromRow, Serialize, Deserialize)] pub struct ContentRevision { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub content_type: String, - pub record_id: String, + pub record_id: SnowflakeId, pub revision_number: i64, pub snapshot: String, - pub created_by: Option, + pub created_by: Option, pub created_at: Timestamp, } #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize)] pub struct RevisionSummary { - pub id: i64, + pub id: SnowflakeId, pub revision_number: i64, - pub created_by: Option, + pub created_by: Option, pub created_at: Timestamp, } pub async fn create_revision( pool: &crate::db::Pool, content_type: &str, - record_id: &str, + record_id: i64, snapshot: &Value, created_by: Option, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let next_rev = next_revision_number(pool, content_type, record_id).await?; @@ -52,7 +51,7 @@ pub async fn create_revision( .map_err(|e| AppError::Internal(anyhow::anyhow!("snapshot serialize: {e}")))?; raisfast_derive::crud_insert!(pool, "content_revisions", [ - "document_id" => &document_id, + "id" => id, "content_type" => content_type, "record_id" => record_id, "revision_number" => next_rev, @@ -61,15 +60,13 @@ pub async fn create_revision( "created_at" => now, ])?; - Ok( - raisfast_derive::crud_find_one!(pool, "content_revisions", ContentRevision, "document_id" => &document_id)?, - ) + Ok(raisfast_derive::crud_find_one!(pool, "content_revisions", ContentRevision, "id" => id)?) } async fn next_revision_number( pool: &crate::db::Pool, content_type: &str, - record_id: &str, + record_id: i64, ) -> AppResult { raisfast_derive::check_schema!( "content_revisions", @@ -94,7 +91,7 @@ async fn next_revision_number( pub async fn list_revisions( pool: &crate::db::Pool, content_type: &str, - record_id: &str, + record_id: i64, ) -> AppResult> { Ok( raisfast_derive::crud_find_all!(pool, "content_revisions", ContentRevision, "content_type" => content_type, and: ["record_id" => record_id], order_by: "revision_number DESC")?, @@ -104,7 +101,7 @@ pub async fn list_revisions( pub async fn get_revision( pool: &crate::db::Pool, content_type: &str, - record_id: &str, + record_id: i64, revision_id: i64, ) -> AppResult> { Ok( @@ -156,7 +153,7 @@ pub fn compute_diff(old: &Value, new: &Value) -> Value { pub async fn delete_revisions( pool: &crate::db::Pool, content_type: &str, - record_id: &str, + record_id: i64, ) -> AppResult { let result = raisfast_derive::crud_delete!(pool, "content_revisions", "content_type" => content_type, and: ["record_id" => record_id])?; Ok(result.rows_affected()) diff --git a/src/models/currencies.rs b/src/models/currencies.rs index d13913d8..3b050a14 100644 --- a/src/models/currencies.rs +++ b/src/models/currencies.rs @@ -2,12 +2,12 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct Currency { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub code: String, pub name: String, pub decimals: i64, @@ -41,7 +41,6 @@ pub async fn find_all(pool: &crate::db::Pool) -> AppResult> { raisfast_derive::check_schema!( "currencies", "id", - "document_id", "code", "name", "decimals", @@ -59,12 +58,12 @@ pub async fn create( name: &str, decimals: i64, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "currencies", [ - "document_id" => &document_id, + "id" => id, "code" => code, "name" => name, "decimals" => decimals, @@ -73,8 +72,7 @@ pub async fn create( ] )?; - raisfast_derive::crud_find_one!(pool, "currencies", Currency, "document_id" => &document_id) - .map_err(Into::into) + raisfast_derive::crud_find_one!(pool, "currencies", Currency, "id" => id).map_err(Into::into) } pub async fn update( @@ -156,7 +154,7 @@ mod tests { assert!(c.is_active); let found = find_by_code(&pool, "CNY").await.unwrap().unwrap(); - assert_eq!(found.document_id, c.document_id); + assert_eq!(found.id, c.id); } #[tokio::test] @@ -205,14 +203,14 @@ mod tests { let user = crate::models::user::create( &pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - crate::models::wallet::create(&pool, user.id, "CNY") + crate::models::wallet::create(&pool, *user.id, "CNY") .await .unwrap(); diff --git a/src/models/email_verification.rs b/src/models/email_verification.rs index b9b37287..01e95d7d 100644 --- a/src/models/email_verification.rs +++ b/src/models/email_verification.rs @@ -6,15 +6,15 @@ use sqlx::FromRow; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; use crate::utils::id; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// Email verification token database row model #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] #[non_exhaustive] pub struct EmailVerificationToken { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub token: String, pub email: String, pub expires_at: Timestamp, @@ -29,7 +29,7 @@ pub async fn create( email: &str, expires_in_secs: i64, ) -> AppResult { - let (document_id, now) = id::new_document_id_and_timestamp(); + let (id, now) = id::new_id_and_timestamp(); let mut token_bytes = [0u8; 32]; getrandom::getrandom(&mut token_bytes).map_err(|e| { @@ -42,7 +42,7 @@ pub async fn create( let expires_at = crate::utils::tz::now_utc() + chrono::Duration::seconds(expires_in_secs); raisfast_derive::crud_insert!(pool, "email_verification_tokens", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "token" => &token, "email" => email, @@ -116,14 +116,14 @@ mod tests { let user = crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - user.id + *user.id } #[tokio::test] @@ -144,7 +144,7 @@ mod tests { let user_id = insert_user(&pool).await; let row = create(&pool, user_id, "ev2@test.com", 3600).await.unwrap(); assert!(row.verified_at.is_none()); - super::mark_verified(&pool, row.id).await.unwrap(); + super::mark_verified(&pool, *row.id).await.unwrap(); let found = find_by_token(&pool, &row.token).await.unwrap(); assert!(found.is_none()); } diff --git a/src/models/media.rs b/src/models/media.rs index 5abf6a08..a9834c7f 100644 --- a/src/models/media.rs +++ b/src/models/media.rs @@ -11,14 +11,14 @@ use serde::{Deserialize, Serialize}; use crate::db::dialect::ph; use crate::db::tenant::tenant_filter_ph; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Media { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub filename: String, pub filepath: String, pub mimetype: String, @@ -38,13 +38,13 @@ pub async fn create( cmd: &crate::commands::CreateMediaCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "media", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "filename" => &cmd.filename, "filepath" => &cmd.filepath, @@ -57,7 +57,8 @@ pub async fn create( tenant: tenant_id )?; - let media = raisfast_derive::crud_find_one!(pool, "media", Media, "document_id" => document_id, tenant: tenant_id)?; + let media = + raisfast_derive::crud_find_one!(pool, "media", Media, "id" => id, tenant: tenant_id)?; Ok(media) } @@ -195,7 +196,7 @@ mod tests { ) .await .unwrap(); - user.id + *user.id } fn make_cmd(user_id: i64, filename: &str) -> CreateMediaCmd { @@ -217,7 +218,7 @@ mod tests { let media = create(&pool, &make_cmd(uid, "photo.png"), None) .await .unwrap(); - let found = find_by_id(&pool, media.id, None).await.unwrap().unwrap(); + let found = find_by_id(&pool, *media.id, None).await.unwrap().unwrap(); assert_eq!(found.id, media.id); assert_eq!(found.filename, "photo.png"); assert_eq!(found.user_id, uid); @@ -261,8 +262,8 @@ mod tests { let media = create(&pool, &make_cmd(uid, "gone.png"), None) .await .unwrap(); - delete(&pool, media.id, None).await.unwrap(); - let found = find_by_id(&pool, media.id, None).await.unwrap(); + delete(&pool, *media.id, None).await.unwrap(); + let found = find_by_id(&pool, *media.id, None).await.unwrap(); assert!(found.is_none()); } diff --git a/src/models/oauth.rs b/src/models/oauth.rs index 983a172d..13577e3f 100644 --- a/src/models/oauth.rs +++ b/src/models/oauth.rs @@ -7,14 +7,14 @@ use sqlx::FromRow; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// OAuth account binding record #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct OAuthAccount { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub provider: String, pub provider_user_id: String, pub email: Option, @@ -31,11 +31,10 @@ pub struct OAuthAccount { /// OAuth short-lived state record #[derive(Debug, FromRow, Serialize, Deserialize)] pub struct OAuthState { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub provider: String, pub code_verifier: String, - pub user_id: Option, + pub user_id: Option, pub created_at: Timestamp, pub expires_at: Timestamp, } @@ -47,41 +46,35 @@ pub async fn create_state( code_verifier: &str, user_id: Option, expires_at: &str, -) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); +) -> AppResult { + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "oauth_states", [ - "document_id" => &document_id, + "id" => id, "provider" => provider, "code_verifier" => code_verifier, "user_id" => user_id, "expires_at" => expires_at, "created_at" => now, ])?; - Ok(document_id) + Ok(id) } -/// Find and delete a state by document_id (one-time use) -pub async fn consume_state( - pool: &crate::db::Pool, - document_id: &str, -) -> AppResult> { - raisfast_derive::check_schema!("oauth_states", "document_id", "expires_at"); +/// Find and delete a state by id (one-time use) +pub async fn consume_state(pool: &crate::db::Pool, id: i64) -> AppResult> { + raisfast_derive::check_schema!("oauth_states", "id", "expires_at"); let sql = format!( - "SELECT * FROM oauth_states WHERE document_id = {} AND expires_at > {}", + "SELECT * FROM oauth_states WHERE id = {} AND expires_at > {}", ph(1), crate::db::dialect::now_fn(), ); let state = sqlx::query_as::<_, OAuthState>(&sql) - .bind(document_id) + .bind(id) .fetch_optional(pool) .await?; if state.is_some() { - let del_sql = format!("DELETE FROM oauth_states WHERE document_id = {}", ph(1)); - sqlx::query(&del_sql) - .bind(document_id) - .execute(pool) - .await?; + let del_sql = format!("DELETE FROM oauth_states WHERE id = {}", ph(1)); + sqlx::query(&del_sql).bind(id).execute(pool).await?; } Ok(state) @@ -117,7 +110,7 @@ pub async fn find_by_user_id(pool: &crate::db::Pool, user_id: i64) -> AppResult< /// Parameters for creating an OAuth account binding pub struct CreateOAuthAccountParams<'a> { - pub user_id: i64, + pub user_id: SnowflakeId, pub provider: &'a str, pub provider_user_id: &'a str, pub email: Option<&'a str>, @@ -134,10 +127,10 @@ pub async fn create_account( pool: &crate::db::Pool, params: CreateOAuthAccountParams<'_>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "oauth_accounts", [ - "document_id" => &document_id, + "id" => id, "user_id" => params.user_id, "provider" => params.provider, "provider_user_id" => params.provider_user_id, @@ -152,14 +145,12 @@ pub async fn create_account( "updated_at" => now, ])?; - Ok( - raisfast_derive::crud_find_one!(pool, "oauth_accounts", OAuthAccount, "document_id" => &document_id)?, - ) + Ok(raisfast_derive::crud_find_one!(pool, "oauth_accounts", OAuthAccount, "id" => id)?) } /// Parameters for updating an OAuth account binding pub struct UpdateOAuthAccountParams<'a> { - pub id: i64, + pub id: SnowflakeId, pub email: Option<&'a str>, pub display_name: Option<&'a str>, pub avatar_url: Option<&'a str>, @@ -216,18 +207,18 @@ mod tests { async fn insert_user(pool: &crate::db::Pool) -> i64 { let cmd = crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }; let user = crate::models::user::create(pool, &cmd, None).await.unwrap(); - user.id + *user.id } #[tokio::test] async fn create_and_consume_state() { let pool = setup_pool().await; let user_id = insert_user(&pool).await; - let doc_id = create_state( + let state_id = create_state( &pool, "github", "verifier123", @@ -236,18 +227,17 @@ mod tests { ) .await .unwrap(); - let state = consume_state(&pool, &doc_id).await.unwrap().unwrap(); - assert_eq!(state.document_id, doc_id); + let state = consume_state(&pool, state_id).await.unwrap().unwrap(); assert_eq!(state.provider, "github"); assert_eq!(state.code_verifier, "verifier123"); - assert_eq!(state.user_id, Some(user_id)); + assert_eq!(state.user_id, Some(crate::utils::id::SnowflakeId(user_id))); } #[tokio::test] async fn consume_state_twice_returns_none() { let pool = setup_pool().await; let user_id = insert_user(&pool).await; - let doc_id = create_state( + let state_id = create_state( &pool, "github", "verifier123", @@ -256,9 +246,9 @@ mod tests { ) .await .unwrap(); - let first = consume_state(&pool, &doc_id).await.unwrap(); + let first = consume_state(&pool, state_id).await.unwrap(); assert!(first.is_some()); - let second = consume_state(&pool, &doc_id).await.unwrap(); + let second = consume_state(&pool, state_id).await.unwrap(); assert!(second.is_none()); } @@ -269,7 +259,7 @@ mod tests { let account = create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "github", provider_user_id: "github-123", email: Some("user@example.com"), @@ -299,7 +289,7 @@ mod tests { create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "github", provider_user_id: "github-123", email: None, @@ -316,7 +306,7 @@ mod tests { create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "google", provider_user_id: "google-456", email: None, @@ -341,7 +331,7 @@ mod tests { create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "github", provider_user_id: "github-123", email: None, @@ -374,7 +364,7 @@ mod tests { create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "github", provider_user_id: "github-123", email: None, @@ -391,7 +381,7 @@ mod tests { create_account( &pool, CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: "google", provider_user_id: "google-456", email: None, diff --git a/src/models/options.rs b/src/models/options.rs index d0ef91eb..6bf433a6 100644 --- a/src/models/options.rs +++ b/src/models/options.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -22,8 +23,7 @@ define_enum!( /// Options table row model (with full metadata) #[derive(Debug, Serialize, Deserialize, Clone)] pub struct OptionRow { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub option_key: String, pub value: String, @@ -45,7 +45,6 @@ impl<'r> sqlx::FromRow<'r, sqlx::sqlite::SqliteRow> for OptionRow { use sqlx::Row; Ok(Self { id: row.try_get("id")?, - document_id: row.try_get("document_id")?, tenant_id: row.try_get("tenant_id").ok(), option_key: row.try_get("option_key")?, value: row.try_get("value")?, @@ -67,7 +66,6 @@ impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for OptionRow { use sqlx::Row; Ok(Self { id: row.try_get("id")?, - document_id: row.try_get("document_id")?, tenant_id: row.try_get("tenant_id").ok(), option_key: row.try_get("option_key")?, value: row.try_get("value")?, @@ -89,7 +87,6 @@ impl<'r> sqlx::FromRow<'r, sqlx::mysql::MySqlRow> for OptionRow { use sqlx::Row; Ok(Self { id: row.try_get("id")?, - document_id: row.try_get("document_id")?, tenant_id: row.try_get("tenant_id").ok(), option_key: row.try_get("option_key")?, value: row.try_get("value")?, @@ -174,10 +171,9 @@ mod tests { updated_at: &str, ) { sqlx::query( - "INSERT INTO options (document_id, option_key, value, type, group_name, label, autoload, sort_order, updated_at) \ - VALUES (?, ?, ?, ?, 'test', 'test', ?, 0, ?)", + "INSERT INTO options (option_key, value, type, group_name, label, autoload, sort_order, updated_at) \ + VALUES (?, ?, ?, 'test', 'test', ?, 0, ?)", ) - .bind(crate::utils::id::new_document_id()) .bind(key) .bind(value) .bind(OptionType::Text) @@ -191,7 +187,7 @@ mod tests { #[tokio::test] async fn upsert_and_find_by_key() { let pool = setup_pool().await; - let key = format!("test.{}", crate::utils::id::new_document_id()); + let key = format!("test.{}", crate::utils::id::new_id()); let now_str = crate::utils::tz::now_utc().to_rfc3339(); insert_test_option(&pool, &key, "initial", true, &now_str).await; @@ -210,9 +206,9 @@ mod tests { let now = crate::utils::tz::now_utc(); let now_str = now.to_rfc3339(); - let k1 = format!("test.{}", crate::utils::id::new_document_id()); - let k2 = format!("test.{}", crate::utils::id::new_document_id()); - let k3 = format!("test.{}", crate::utils::id::new_document_id()); + let k1 = format!("test.{}", crate::utils::id::new_id()); + let k2 = format!("test.{}", crate::utils::id::new_id()); + let k3 = format!("test.{}", crate::utils::id::new_id()); insert_test_option(&pool, &k1, "v1", true, &now_str).await; insert_test_option(&pool, &k2, "v2", true, &now_str).await; insert_test_option(&pool, &k3, "v3", true, &now_str).await; @@ -224,7 +220,7 @@ mod tests { #[tokio::test] async fn upsert_overwrites() { let pool = setup_pool().await; - let key = format!("test.{}", crate::utils::id::new_document_id()); + let key = format!("test.{}", crate::utils::id::new_id()); let now = crate::utils::tz::now_utc(); let now_str = now.to_rfc3339(); @@ -238,7 +234,7 @@ mod tests { #[tokio::test] async fn delete_by_key_removes() { let pool = setup_pool().await; - let key = format!("test.{}", crate::utils::id::new_document_id()); + let key = format!("test.{}", crate::utils::id::new_id()); let now_str = crate::utils::tz::now_utc().to_rfc3339(); insert_test_option(&pool, &key, "val", true, &now_str).await; @@ -253,9 +249,9 @@ mod tests { let pool = setup_pool().await; let now_str = crate::utils::tz::now_utc().to_rfc3339(); - let k1 = format!("test.{}", crate::utils::id::new_document_id()); - let k2 = format!("test.{}", crate::utils::id::new_document_id()); - let k3 = format!("test.{}", crate::utils::id::new_document_id()); + let k1 = format!("test.{}", crate::utils::id::new_id()); + let k2 = format!("test.{}", crate::utils::id::new_id()); + let k3 = format!("test.{}", crate::utils::id::new_id()); insert_test_option(&pool, &k1, "v1", true, &now_str).await; insert_test_option(&pool, &k2, "v2", true, &now_str).await; insert_test_option(&pool, &k3, "v3", false, &now_str).await; diff --git a/src/models/order.rs b/src/models/order.rs index d7e0409a..f6387e4e 100644 --- a/src/models/order.rs +++ b/src/models/order.rs @@ -5,6 +5,7 @@ use ts_rs::TS; use crate::db::dialect::ph; use crate::db::tenant::tenant_filter_ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -23,10 +24,9 @@ define_enum!( #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Order { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub order_no: String, pub subtotal: i64, pub discount_amount: i64, @@ -44,9 +44,9 @@ pub struct Order { pub admin_remark: Option, pub delivery_data: Option, pub tax_amount: i64, - pub coupon_id: Option, - pub shipping_address_id: Option, - pub billing_address_id: Option, + pub coupon_id: Option, + pub shipping_address_id: Option, + pub billing_address_id: Option, pub paid_at: Option, pub completed_at: Option, pub cancelled_at: Option, @@ -66,15 +66,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "orders", Order, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn find_by_order_no( pool: &crate::db::Pool, order_no: &str, @@ -128,13 +119,12 @@ pub async fn insert( cmd: &crate::commands::CreateOrderCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "orders", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "order_no" => &cmd.order_no, "subtotal" => cmd.subtotal, @@ -156,13 +146,11 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| { - crate::errors::app_error::AppError::Internal(anyhow::anyhow!( - "order not found after insert" - )) - }) + find_by_id(pool, id, tenant_id).await?.ok_or_else(|| { + crate::errors::app_error::AppError::Internal(anyhow::anyhow!( + "order not found after insert" + )) + }) } fn validate_timestamp_col(col: &str) -> AppResult<()> { @@ -269,70 +257,17 @@ pub async fn update_delivery_data( Ok(()) } -pub async fn tx_find_id_by_document_id( - tx: &mut crate::db::pool::DbConnection, - document_id: &str, -) -> AppResult> { - let sql = format!("SELECT id FROM orders WHERE document_id = {}", ph(1)); - Ok(raisfast_derive::crud_scalar!( - &mut *tx, - i64, - &sql, - [document_id], - fetch_optional - )?) -} - -pub async fn tx_find_by_document_id( - tx: &mut crate::db::pool::DbConnection, - document_id: &str, -) -> AppResult> { - raisfast_derive::crud_find!(&mut *tx, "orders", Order, "document_id" => document_id) - .map_err(Into::into) -} - -pub async fn tx_update_status( - tx: &mut crate::db::pool::DbConnection, - id: i64, - status: OrderStatus, - timestamp_col: Option<&str>, -) -> AppResult<()> { - raisfast_derive::check_schema!("orders", "status", "updated_at", "id"); - let sql = if let Some(col) = timestamp_col { - validate_timestamp_col(col)?; - format!( - "UPDATE orders SET status = {}, {} = datetime('now'), updated_at = datetime('now') WHERE id = {}", - ph(1), - col, - ph(2) - ) - } else { - format!( - "UPDATE orders SET status = {}, updated_at = datetime('now') WHERE id = {}", - ph(1), - ph(2) - ) - }; - sqlx::query(&sql) - .bind(status) - .bind(id) - .execute(&mut *tx) - .await?; - Ok(()) -} - pub async fn tx_insert( tx: &mut crate::db::pool::DbConnection, cmd: &crate::commands::CreateOrderCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( &mut *tx, "orders", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "order_no" => &cmd.order_no, "subtotal" => cmd.subtotal, @@ -356,14 +291,14 @@ pub async fn tx_insert( )?; let sql = if tenant_id.is_some() { format!( - "SELECT * FROM orders WHERE document_id = {} AND tenant_id = {}", + "SELECT * FROM orders WHERE id = {} AND tenant_id = {}", ph(1), ph(2) ) } else { - format!("SELECT * FROM orders WHERE document_id = {}", ph(1)) + format!("SELECT * FROM orders WHERE id = {}", ph(1)) }; - raisfast_derive::crud_query!(&mut *tx, Order, &sql, [&document_id], fetch_one, tenant: tenant_id) + raisfast_derive::crud_query!(&mut *tx, Order, &sql, [id], fetch_one, tenant: tenant_id) .map_err(Into::into) } @@ -471,19 +406,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -535,23 +465,14 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); - assert_eq!(found.id, o.id); - assert_eq!(found.user_id, uid); - assert_eq!(found.total_amount, 1000); - assert_eq!(found.status, OrderStatus::Pending); - } - - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let uid = seed_user(&pool).await; - let o = seed_order(&pool, uid).await; - let found = super::find_by_document_id(&pool, &o.document_id, None) + let found = super::find_by_id(&pool, *o.id, None) .await .unwrap() .unwrap(); assert_eq!(found.id, o.id); + assert_eq!(found.user_id, uid); + assert_eq!(found.total_amount, 1000); + assert_eq!(found.status, OrderStatus::Pending); } #[tokio::test] @@ -577,17 +498,6 @@ mod tests { ); } - #[tokio::test] - async fn find_by_document_id_not_found() { - let pool = setup_pool().await; - assert!( - super::find_by_document_id(&pool, "nonexistent", None) - .await - .unwrap() - .is_none() - ); - } - #[tokio::test] async fn insert_sets_defaults() { let pool = setup_pool().await; @@ -647,10 +557,13 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "paid", Some("paid_at"), None) + super::update_status(&pool, *o.id, "paid", Some("paid_at"), None) .await .unwrap(); - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *o.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.status, OrderStatus::Paid); assert!(found.paid_at.is_some()); } @@ -660,10 +573,13 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "cancelled", Some("cancelled_at"), None) + super::update_status(&pool, *o.id, "cancelled", Some("cancelled_at"), None) .await .unwrap(); - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *o.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.status, OrderStatus::Cancelled); assert!(found.cancelled_at.is_some()); } @@ -673,12 +589,12 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "expired", None, None) + super::update_status(&pool, *o.id, "expired", None, None) .await .unwrap(); - let status = get_status(&pool, o.id).await; + let status = get_status(&pool, *o.id).await; assert_eq!(status, "expired"); - let expired_at = get_optional_field(&pool, o.id, "expired_at").await; + let expired_at = get_optional_field(&pool, *o.id, "expired_at").await; assert!(expired_at.is_none()); } @@ -687,13 +603,16 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "paid", Some("paid_at"), None) + super::update_status(&pool, *o.id, "paid", Some("paid_at"), None) .await .unwrap(); - super::update_shipped(&pool, o.id, Some("TRACK123"), Some("FedEx"), None) + super::update_shipped(&pool, *o.id, Some("TRACK123"), Some("FedEx"), None) .await .unwrap(); - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *o.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.status, OrderStatus::Shipped); assert_eq!(found.tracking_no.unwrap(), "TRACK123"); assert_eq!(found.carrier.unwrap(), "FedEx"); @@ -704,10 +623,13 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_admin_remark(&pool, o.id, "fraud suspected", None) + super::update_admin_remark(&pool, *o.id, "fraud suspected", None) .await .unwrap(); - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *o.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.admin_remark.unwrap(), "fraud suspected"); } @@ -716,10 +638,13 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_delivery_data(&pool, o.id, r#"{"tracking_url":"https://t.co/abc"}"#, None) + super::update_delivery_data(&pool, *o.id, r#"{"tracking_url":"https://t.co/abc"}"#, None) .await .unwrap(); - let found = super::find_by_id(&pool, o.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *o.id, None) + .await + .unwrap() + .unwrap(); assert_eq!( found.delivery_data.unwrap(), r#"{"tracking_url":"https://t.co/abc"}"# @@ -782,7 +707,7 @@ mod tests { let uid = seed_user(&pool).await; for _ in 0..3 { let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "paid", Some("paid_at"), None) + super::update_status(&pool, *o.id, "paid", Some("paid_at"), None) .await .unwrap(); } @@ -811,22 +736,22 @@ mod tests { let pool = setup_pool().await; let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - assert_eq!(get_status(&pool, o.id).await, "pending"); + assert_eq!(get_status(&pool, *o.id).await, "pending"); - super::update_status(&pool, o.id, "paid", Some("paid_at"), None) + super::update_status(&pool, *o.id, "paid", Some("paid_at"), None) .await .unwrap(); - assert_eq!(get_status(&pool, o.id).await, "paid"); + assert_eq!(get_status(&pool, *o.id).await, "paid"); - super::update_shipped(&pool, o.id, Some("TRK001"), Some("UPS"), None) + super::update_shipped(&pool, *o.id, Some("TRK001"), Some("UPS"), None) .await .unwrap(); - assert_eq!(get_status(&pool, o.id).await, "shipped"); + assert_eq!(get_status(&pool, *o.id).await, "shipped"); - super::update_status(&pool, o.id, "completed", Some("completed_at"), None) + super::update_status(&pool, *o.id, "completed", Some("completed_at"), None) .await .unwrap(); - assert_eq!(get_status(&pool, o.id).await, "completed"); + assert_eq!(get_status(&pool, *o.id).await, "completed"); } #[tokio::test] @@ -835,17 +760,17 @@ mod tests { let uid = seed_user(&pool).await; let o = seed_order(&pool, uid).await; - super::update_status(&pool, o.id, "paid", Some("paid_at"), None) + super::update_status(&pool, *o.id, "paid", Some("paid_at"), None) .await .unwrap(); - super::update_status(&pool, o.id, "refunding", Some("refunding_at"), None) + super::update_status(&pool, *o.id, "refunding", Some("refunding_at"), None) .await .unwrap(); - assert_eq!(get_status(&pool, o.id).await, "refunding"); + assert_eq!(get_status(&pool, *o.id).await, "refunding"); - super::update_status(&pool, o.id, "refunded", Some("refunded_at"), None) + super::update_status(&pool, *o.id, "refunded", Some("refunded_at"), None) .await .unwrap(); - assert_eq!(get_status(&pool, o.id).await, "refunded"); + assert_eq!(get_status(&pool, *o.id).await, "refunded"); } } diff --git a/src/models/order_item.rs b/src/models/order_item.rs index 14b5e73c..cb389bee 100644 --- a/src/models/order_item.rs +++ b/src/models/order_item.rs @@ -3,17 +3,17 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct OrderItem { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub order_id: i64, - pub product_id: Option, - pub variant_id: Option, + pub order_id: SnowflakeId, + pub product_id: Option, + pub variant_id: Option, pub title: String, pub description: Option, pub sku: Option, @@ -40,13 +40,12 @@ pub async fn insert( cmd: &crate::commands::CreateOrderItemCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "order_items", [ - "document_id" => &document_id, + "id" => id, "order_id" => cmd.order_id, "product_id" => cmd.product_id, "variant_id" => cmd.variant_id, @@ -63,7 +62,7 @@ pub async fn insert( ], tenant: tenant_id )?; - raisfast_derive::crud_find_one!(pool, "order_items", OrderItem, "document_id" => &document_id, tenant: tenant_id) + raisfast_derive::crud_find_one!(pool, "order_items", OrderItem, "id" => id, tenant: tenant_id) .map_err(Into::into) } @@ -83,13 +82,12 @@ pub async fn tx_insert( cmd: &crate::commands::CreateOrderItemCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( &mut *tx, "order_items", [ - "document_id" => &document_id, + "id" => id, "order_id" => cmd.order_id, "product_id" => cmd.product_id, "variant_id" => cmd.variant_id, @@ -106,7 +104,7 @@ pub async fn tx_insert( ], tenant: tenant_id )?; - raisfast_derive::crud_find_one!(&mut *tx, "order_items", OrderItem, "document_id" => &document_id, tenant: tenant_id) + raisfast_derive::crud_find_one!(&mut *tx, "order_items", OrderItem, "id" => id, tenant: tenant_id) .map_err(Into::into) } @@ -128,19 +126,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -249,7 +242,7 @@ mod tests { .unwrap(); assert_eq!(item.order_id, order_id); - assert_eq!(item.product_id, Some(pid)); + assert_eq!(item.product_id, Some(crate::utils::id::SnowflakeId(pid))); assert_eq!(item.title, "Widget"); assert_eq!(item.unit_price, 1000); assert_eq!(item.quantity, 2); diff --git a/src/models/page.rs b/src/models/page.rs index 0d5a8349..5187af8f 100644 --- a/src/models/page.rs +++ b/src/models/page.rs @@ -10,6 +10,7 @@ use crate::db::dialect::ph; use crate::db::tenant::tenant_filter_ph; use crate::errors::app_error::{AppError, AppResult}; use crate::models::post::CommentOpenStatus; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -22,8 +23,7 @@ define_enum!( #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Page { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub title: String, pub slug: String, @@ -33,11 +33,11 @@ pub struct Page { pub meta_description: Option, pub og_image: Option, pub template: String, - pub parent_id: Option, + pub parent_id: Option, pub sort_order: i64, pub status: PageStatus, - pub created_by: i64, - pub updated_by: Option, + pub created_by: SnowflakeId, + pub updated_by: Option, pub cover_image: Option, pub password: Option, pub comment_status: CommentOpenStatus, @@ -326,16 +326,6 @@ pub async fn find_by_id( Ok(raisfast_derive::crud_find!(pool, "pages", Page, "id" => id, tenant: tenant_id)?) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - Ok( - raisfast_derive::crud_find!(pool, "pages", Page, "document_id" => document_id, tenant: tenant_id)?, - ) -} - pub async fn list_published( pool: &crate::db::Pool, page: i64, @@ -379,7 +369,7 @@ pub async fn create( cmd: &CreatePageCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let published_at = if cmd.status == PageStatus::Published { Some(now) } else { @@ -390,7 +380,7 @@ pub async fn create( pool, "pages", [ - "document_id" => &document_id, + "id" => id, "title" => &cmd.title, "slug" => &cmd.slug, "content" => &cmd.content, @@ -412,7 +402,7 @@ pub async fn create( tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) + find_by_id(pool, id, tenant_id) .await? .ok_or_else(|| AppError::not_found("page")) } @@ -686,20 +676,14 @@ mod tests { } async fn create_user(pool: &crate::db::Pool) -> i64 { - let uid = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email')", ) - .bind(&uid) + .bind(id) .execute(pool) .await .unwrap(); - - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&uid) - .fetch_one(pool) - .await - .unwrap(); id } @@ -745,19 +729,6 @@ mod tests { assert_eq!(found.unwrap().id, page.id); } - #[tokio::test] - async fn find_by_document_id_test() { - let pool = setup_pool().await; - let uid = create_user(&pool).await; - let page = create_test_page(&pool, "Contact", "contact", "draft", uid).await; - - let found = super::find_by_document_id(&pool, &page.document_id, None) - .await - .unwrap(); - assert!(found.is_some()); - assert_eq!(found.unwrap().slug, "contact"); - } - #[tokio::test] async fn list_published_excludes_drafts() { let pool = setup_pool().await; @@ -780,7 +751,7 @@ mod tests { let updated = update( &pool, &UpdatePageCmd { - id: page.id, + id: *page.id, title: Some("New Title".to_string()), slug: None, content: None, @@ -809,7 +780,7 @@ mod tests { let uid = create_user(&pool).await; let page = create_test_page(&pool, "To Delete", "delete-me", "published", uid).await; - delete(&pool, page.id, None).await.unwrap(); + delete(&pool, *page.id, None).await.unwrap(); let found = find_by_slug(&pool, "delete-me", None).await.unwrap(); assert!(found.is_none()); } @@ -822,7 +793,7 @@ mod tests { assert_eq!(page.status, PageStatus::Draft); - let updated = update_status(&pool, page.id, PageStatus::Published, None, None) + let updated = update_status(&pool, *page.id, PageStatus::Published, None, None) .await .unwrap(); assert_eq!(updated.status, PageStatus::Published); diff --git a/src/models/password_reset.rs b/src/models/password_reset.rs index d3e0c47d..caec4f24 100644 --- a/src/models/password_reset.rs +++ b/src/models/password_reset.rs @@ -8,15 +8,15 @@ use sqlx::FromRow; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; use crate::utils::id; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// Password reset token full database row model #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] #[non_exhaustive] pub struct PasswordResetToken { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub token: String, pub expires_at: Timestamp, pub used_at: Option, @@ -25,13 +25,13 @@ pub struct PasswordResetToken { /// Create a new password reset token /// -/// Generates a document_id and a 32-byte random token. Validity is controlled by `expires_in_secs`. +/// Generates a Snowflake ID and a 32-byte random token. Validity is controlled by `expires_in_secs`. pub async fn create( pool: &crate::db::Pool, user_id: i64, expires_in_secs: i64, ) -> AppResult { - let (document_id, now) = id::new_document_id_and_timestamp(); + let (id, now) = id::new_id_and_timestamp(); let mut token_bytes = [0u8; 32]; getrandom::getrandom(&mut token_bytes).map_err(|e| { @@ -44,7 +44,7 @@ pub async fn create( let expires_at = crate::utils::tz::now_utc() + chrono::Duration::seconds(expires_in_secs); raisfast_derive::crud_insert!(pool, "password_reset_tokens", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "token" => &token, "expires_at" => expires_at, @@ -117,14 +117,14 @@ mod tests { let user = crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - user.id + *user.id } #[tokio::test] @@ -132,7 +132,7 @@ mod tests { let pool = setup_pool().await; let user_id = insert_user(&pool).await; let row = create(&pool, user_id, 3600).await.unwrap(); - assert!(row.id > 0); + assert!(*row.id > 0); assert_eq!(row.user_id, user_id); assert!(!row.token.is_empty()); assert!(row.used_at.is_none()); @@ -149,7 +149,7 @@ mod tests { let row = create(&pool, user_id, 3600).await.unwrap(); assert!(row.used_at.is_none()); - super::mark_used(&pool, row.id).await.unwrap(); + super::mark_used(&pool, *row.id).await.unwrap(); let found = find_by_token(&pool, &row.token).await.unwrap(); assert!( diff --git a/src/models/payment_channel.rs b/src/models/payment_channel.rs index 0e4a7da5..85d8140d 100644 --- a/src/models/payment_channel.rs +++ b/src/models/payment_channel.rs @@ -1,12 +1,12 @@ use serde::{Deserialize, Serialize}; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct PaymentChannel { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub provider: String, pub name: String, @@ -30,14 +30,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "payment_channels", PaymentChannel, "document_id" => document_id, tenant: tenant_id).map_err(Into::into) -} - pub async fn find_all_active( pool: &crate::db::Pool, tenant_id: Option<&str>, @@ -72,7 +64,7 @@ pub async fn insert( cmd: &crate::commands::CreatePaymentChannelCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let is_live_val = if cmd.is_live { 1_i64 } else { 0_i64 }; let is_active_val = if cmd.is_active { 1_i64 } else { 0_i64 }; let now = crate::utils::tz::now_utc(); @@ -80,7 +72,7 @@ pub async fn insert( pool, "payment_channels", [ - "document_id" => &document_id, + "id" => id, "provider" => &cmd.provider, "name" => &cmd.name, "is_live" => is_live_val, @@ -94,13 +86,11 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| { - crate::errors::app_error::AppError::Internal(anyhow::anyhow!( - "inserted row not found: {document_id}" - )) - }) + find_by_id(pool, id, tenant_id).await?.ok_or_else(|| { + crate::errors::app_error::AppError::Internal(anyhow::anyhow!( + "inserted row not found: {id}" + )) + }) } pub async fn update( @@ -173,7 +163,7 @@ mod tests { async fn insert_and_find_by_id() { let pool = setup_pool().await; let ch = seed_channel(&pool, "stripe").await; - let found = super::find_by_id(&pool, ch.id, None) + let found = super::find_by_id(&pool, *ch.id, None) .await .unwrap() .unwrap(); @@ -184,17 +174,6 @@ mod tests { assert_eq!(found.version, 1); } - #[tokio::test] - async fn find_by_document_id_works() { - let pool = setup_pool().await; - let ch = seed_channel(&pool, "alipay").await; - let found = super::find_by_document_id(&pool, &ch.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, ch.id); - } - #[tokio::test] async fn find_by_id_not_found() { let pool = setup_pool().await; @@ -206,17 +185,6 @@ mod tests { ); } - #[tokio::test] - async fn find_by_document_id_not_found() { - let pool = setup_pool().await; - assert!( - super::find_by_document_id(&pool, "nonexistent", None) - .await - .unwrap() - .is_none() - ); - } - #[tokio::test] async fn find_all_active_channels() { let pool = setup_pool().await; @@ -268,7 +236,7 @@ mod tests { let ok = super::update( &pool, &crate::commands::UpdatePaymentChannelCmd { - id: ch.id, + id: *ch.id, provider: "paypal".into(), name: "PayPal Live".into(), is_live: true, @@ -284,7 +252,7 @@ mod tests { .await .unwrap(); assert!(ok); - let found = super::find_by_id(&pool, ch.id, None) + let found = super::find_by_id(&pool, *ch.id, None) .await .unwrap() .unwrap(); @@ -303,7 +271,7 @@ mod tests { let ok = super::update( &pool, &crate::commands::UpdatePaymentChannelCmd { - id: ch.id, + id: *ch.id, provider: "stripe".into(), name: "name".into(), is_live: false, @@ -325,10 +293,10 @@ mod tests { async fn delete_removes_channel() { let pool = setup_pool().await; let ch = seed_channel(&pool, "stripe").await; - let ok = super::delete_by_id(&pool, ch.id, None).await.unwrap(); + let ok = super::delete_by_id(&pool, *ch.id, None).await.unwrap(); assert!(ok); assert!( - super::find_by_id(&pool, ch.id, None) + super::find_by_id(&pool, *ch.id, None) .await .unwrap() .is_none() diff --git a/src/models/payment_order.rs b/src/models/payment_order.rs index 89895a5b..41e7e9f7 100644 --- a/src/models/payment_order.rs +++ b/src/models/payment_order.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -35,15 +36,14 @@ define_enum!( #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct PaymentOrder { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub order_id: Option, pub title: String, pub amount: i64, pub currency: String, - pub channel_id: i64, + pub channel_id: SnowflakeId, pub provider: String, pub provider_order_id: Option, pub provider_method: Option, @@ -76,15 +76,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "payment_orders", PaymentOrder, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn find_by_idempotency_key( pool: &crate::db::Pool, key: &str, @@ -146,13 +137,13 @@ pub async fn insert( cmd: &crate::commands::CreatePaymentOrderCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( pool, "payment_orders", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "order_id" => &cmd.order_id, "title" => &cmd.title, @@ -175,13 +166,11 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| { - crate::errors::app_error::AppError::Internal(anyhow::anyhow!( - "inserted row not found: {document_id}" - )) - }) + find_by_id(pool, id, tenant_id).await?.ok_or_else(|| { + crate::errors::app_error::AppError::Internal(anyhow::anyhow!( + "inserted row not found: {id}" + )) + }) } pub async fn update_provider_order_id( @@ -243,19 +232,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -326,7 +310,7 @@ mod tests { let uid = seed_user(&pool).await; let ch_id = seed_channel(&pool).await; let order = seed_payment_order(&pool, uid, ch_id, 1000).await; - let found = super::find_by_id(&pool, order.id, None) + let found = super::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -338,19 +322,6 @@ mod tests { assert_eq!(found.channel_id, ch_id); } - #[tokio::test] - async fn find_by_document_id_works() { - let pool = setup_pool().await; - let uid = seed_user(&pool).await; - let ch_id = seed_channel(&pool).await; - let order = seed_payment_order(&pool, uid, ch_id, 500).await; - let found = super::find_by_document_id(&pool, &order.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, order.id); - } - #[tokio::test] async fn find_by_idempotency_key_works() { let pool = setup_pool().await; @@ -370,7 +341,7 @@ mod tests { let uid = seed_user(&pool).await; let ch_id = seed_channel(&pool).await; let order = seed_payment_order(&pool, uid, ch_id, 500).await; - super::update_provider_order_id(&pool, order.id, "pi_test123", None, None) + super::update_provider_order_id(&pool, *order.id, "pi_test123", None, None) .await .unwrap(); let found = super::find_by_provider_order_id(&pool, "pi_test123", None) @@ -413,7 +384,7 @@ mod tests { let mut tx = pool.begin().await.unwrap(); let rows = super::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -422,7 +393,7 @@ mod tests { .unwrap(); tx.commit().await.unwrap(); assert_eq!(rows, 1); - let found = super::find_by_id(&pool, order.id, None) + let found = super::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -440,7 +411,7 @@ mod tests { let mut tx = pool.begin().await.unwrap(); let rows = super::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Cancelled, Some("cancelled_at"), PaymentStatus::Pending, @@ -449,7 +420,7 @@ mod tests { .unwrap(); tx.commit().await.unwrap(); assert_eq!(rows, 1); - let found = super::find_by_id(&pool, order.id, None) + let found = super::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -465,14 +436,14 @@ mod tests { let order = seed_payment_order(&pool, uid, ch_id, 1000).await; super::update_provider_order_id( &pool, - order.id, + *order.id, "pi_abc123", Some(r#"{"status":"requires_action"}"#), None, ) .await .unwrap(); - let found = super::find_by_id(&pool, order.id, None) + let found = super::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -524,7 +495,7 @@ mod tests { let mut tx = pool.begin().await.unwrap(); let rows = super::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -561,13 +532,13 @@ mod tests { let order = seed_payment_order(&pool, uid, ch_id, 2000).await; assert_eq!(order.status, PaymentStatus::Pending); - super::update_provider_order_id(&pool, order.id, "pi_xyz", None, None) + super::update_provider_order_id(&pool, *order.id, "pi_xyz", None, None) .await .unwrap(); let mut tx = pool.begin().await.unwrap(); let rows = super::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -577,7 +548,7 @@ mod tests { assert_eq!(rows, 1); tx.commit().await.unwrap(); - let found = super::find_by_id(&pool, order.id, None) + let found = super::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); diff --git a/src/models/payment_refund.rs b/src/models/payment_refund.rs index cf8edfe7..e5a85a54 100644 --- a/src/models/payment_refund.rs +++ b/src/models/payment_refund.rs @@ -3,22 +3,22 @@ use serde::{Deserialize, Serialize}; use crate::db::dialect::ph; use crate::db::tenant::tenant_filter_ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct PaymentRefund { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub payment_order_id: i64, + pub payment_order_id: SnowflakeId, pub order_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub amount: i64, pub currency: String, pub reason: Option, pub provider_refund_id: Option, pub status: String, - pub payment_tx_id: Option, + pub payment_tx_id: Option, pub metadata: Option, pub created_at: Timestamp, pub updated_at: Timestamp, @@ -56,13 +56,13 @@ pub async fn insert( cmd: &crate::commands::CreatePaymentRefundCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( pool, "payment_refunds", [ - "document_id" => &document_id, + "id" => id, "payment_order_id" => cmd.payment_order_id, "order_id" => &cmd.order_id, "user_id" => cmd.user_id, @@ -78,7 +78,7 @@ pub async fn insert( ], tenant: tenant_id )?; - raisfast_derive::crud_find_one!(pool, "payment_refunds", PaymentRefund, "document_id" => &document_id, tenant: tenant_id).map_err(Into::into) + raisfast_derive::crud_find_one!(pool, "payment_refunds", PaymentRefund, "id" => id, tenant: tenant_id).map_err(Into::into) } pub async fn update_status( @@ -141,13 +141,13 @@ pub async fn tx_insert( cmd: &crate::commands::CreatePaymentRefundCmd, tenant_id: Option<&str>, ) -> AppResult<()> { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( &mut *tx, "payment_refunds", [ - "document_id" => &document_id, + "id" => id, "payment_order_id" => cmd.payment_order_id, "order_id" => &cmd.order_id, "user_id" => cmd.user_id, @@ -193,13 +193,6 @@ pub async fn tx_sum_refunded_by_order( Ok(total) } -pub async fn tx_find_by_document_id( - tx: &mut crate::db::pool::DbConnection, - document_id: &str, -) -> AppResult> { - raisfast_derive::crud_find!(&mut *tx, "payment_refunds", PaymentRefund, "document_id" => document_id).map_err(Into::into) -} - #[cfg(test)] mod tests { use super::*; @@ -209,19 +202,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -323,7 +311,7 @@ mod tests { let ch_id = seed_channel(&pool).await; let po_id = seed_payment_order(&pool, uid, ch_id).await; let refund = seed_refund(&pool, po_id, uid, 500, "pending").await; - let found = super::find_by_id(&pool, refund.id, None) + let found = super::find_by_id(&pool, *refund.id, None) .await .unwrap() .unwrap(); @@ -380,10 +368,10 @@ mod tests { let ch_id = seed_channel(&pool).await; let po_id = seed_payment_order(&pool, uid, ch_id).await; let refund = seed_refund(&pool, po_id, uid, 500, "pending").await; - super::update_status(&pool, refund.id, "succeeded", None) + super::update_status(&pool, *refund.id, "succeeded", None) .await .unwrap(); - let found = super::find_by_id(&pool, refund.id, None) + let found = super::find_by_id(&pool, *refund.id, None) .await .unwrap() .unwrap(); diff --git a/src/models/payment_transaction.rs b/src/models/payment_transaction.rs index 861ecee8..ee0ed107 100644 --- a/src/models/payment_transaction.rs +++ b/src/models/payment_transaction.rs @@ -1,16 +1,16 @@ use serde::{Deserialize, Serialize}; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct PaymentTransaction { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub payment_order_id: i64, + pub payment_order_id: SnowflakeId, pub order_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub tx_type: String, pub amount: i64, pub currency: String, @@ -56,15 +56,6 @@ pub async fn find_by_provider_tx_id( .map_err(Into::into) } -async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "payment_transactions", PaymentTransaction, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn find_all_admin_paginated( pool: &crate::db::Pool, tenant_id: Option<&str>, @@ -88,13 +79,13 @@ pub async fn insert( cmd: &crate::commands::CreatePaymentTransactionCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( pool, "payment_transactions", [ - "document_id" => &document_id, + "id" => id, "payment_order_id" => cmd.payment_order_id, "order_id" => &cmd.order_id, "user_id" => cmd.user_id, @@ -108,13 +99,11 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| { - crate::errors::app_error::AppError::Internal(anyhow::anyhow!( - "inserted row not found: {document_id}" - )) - }) + find_by_id(pool, id, tenant_id).await?.ok_or_else(|| { + crate::errors::app_error::AppError::Internal(anyhow::anyhow!( + "inserted row not found: {id}" + )) + }) } pub async fn tx_insert( @@ -122,13 +111,13 @@ pub async fn tx_insert( cmd: &crate::commands::CreatePaymentTransactionCmd, tenant_id: Option<&str>, ) -> AppResult<()> { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( &mut *tx, "payment_transactions", [ - "document_id" => &document_id, + "id" => id, "payment_order_id" => cmd.payment_order_id, "order_id" => &cmd.order_id, "user_id" => cmd.user_id, @@ -154,19 +143,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -266,7 +250,7 @@ mod tests { let ch_id = seed_channel(&pool).await; let po_id = seed_payment_order(&pool, uid, ch_id).await; let tx = seed_tx(&pool, po_id, uid, "charge", "ch_abc123").await; - let found = super::find_by_id(&pool, tx.id, None) + let found = super::find_by_id(&pool, *tx.id, None) .await .unwrap() .unwrap(); diff --git a/src/models/post.rs b/src/models/post.rs index 2fafd30d..c476b5d8 100644 --- a/src/models/post.rs +++ b/src/models/post.rs @@ -16,6 +16,7 @@ use ts_rs::TS; use crate::db::dialect::ph; use crate::db::tenant::{tenant_filter_aliased_ph, tenant_filter_ph}; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -35,8 +36,7 @@ define_enum!( #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] #[non_exhaustive] pub struct Post { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub title: String, pub slug: String, @@ -44,9 +44,9 @@ pub struct Post { pub excerpt: Option, pub cover_image: Option, pub status: PostStatus, - pub created_by: i64, - pub updated_by: Option, - pub category_id: Option, + pub created_by: SnowflakeId, + pub updated_by: Option, + pub category_id: Option, pub view_count: i64, pub is_pinned: bool, pub password: Option, @@ -68,7 +68,7 @@ pub struct Post { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, utoipa::ToSchema)] pub struct TagBrief { - pub id: i64, + pub id: String, pub name: String, pub slug: String, } @@ -91,15 +91,6 @@ pub async fn find_by_id( Ok(post) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - let post = raisfast_derive::crud_find!(pool, "posts", Post, "document_id" => document_id, tenant: tenant_id)?; - Ok(post) -} - pub async fn create( pool: &crate::db::Pool, cmd: &crate::commands::CreatePostCmd, @@ -107,9 +98,9 @@ pub async fn create( ) -> AppResult { let mut tx = pool.begin().await?; let post = create_tx(&mut tx, cmd, tenant_id).await?; - let doc_id = post.document_id.clone(); + let post_id = post.id; tx.commit().await?; - find_by_document_id(pool, &doc_id, tenant_id) + find_by_id(pool, *post_id, tenant_id) .await? .ok_or_else(|| AppError::not_found("post")) } @@ -119,7 +110,7 @@ pub async fn create_tx( cmd: &crate::commands::CreatePostCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let published_at = if cmd.status == PostStatus::Published { Some(now) } else { @@ -129,7 +120,7 @@ pub async fn create_tx( &mut **tx, "posts", [ - "document_id" => &document_id, + "id" => id, "title" => &cmd.title, "slug" => &cmd.slug, "content" => &cmd.content, @@ -146,19 +137,19 @@ pub async fn create_tx( tenant: tenant_id )?; - let created = find_by_document_id_tx(tx, &document_id, tenant_id) + let created = find_by_id_tx(tx, id, tenant_id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to read created post")))?; Ok(created) } -async fn find_by_document_id_tx( +async fn find_by_id_tx( tx: &mut crate::db::Transaction<'_>, - document_id: &str, + id: i64, tenant_id: Option<&str>, ) -> AppResult> { - raisfast_derive::crud_find!(&mut **tx, "posts", Post, "document_id" => document_id, tenant: tenant_id) + raisfast_derive::crud_find!(&mut **tx, "posts", Post, "id" => id, tenant: tenant_id) .map_err(Into::into) } @@ -206,9 +197,9 @@ pub async fn update_tx( .as_deref() .map(std::string::ToString::to_string) .or(existing.cover_image); - let category_id: Option = cmd.category_id.or(existing.category_id); + let category_id: Option = cmd.category_id.or(existing.category_id.map(|v| *v)); let slug = cmd.slug.as_deref().unwrap_or(&existing.slug); - let updated_by: Option = cmd.updated_by.or(existing.updated_by); + let updated_by: Option = cmd.updated_by.or(existing.updated_by.map(|v| *v)); raisfast_derive::crud_update!( &mut **tx, "posts", @@ -225,7 +216,6 @@ pub async fn update_tx( Ok(Post { id: existing.id, - document_id: existing.document_id, tenant_id: existing.tenant_id, title: title.to_string(), slug: slug.to_string(), @@ -234,8 +224,8 @@ pub async fn update_tx( cover_image, status: new_status, created_by: existing.created_by, - updated_by, - category_id, + updated_by: updated_by.map(SnowflakeId), + category_id: category_id.map(SnowflakeId), view_count: existing.view_count, is_pinned: existing.is_pinned, password: existing.password, @@ -299,7 +289,7 @@ pub async fn sync_tags_tx( #[derive(Debug, FromRow)] pub struct TagRow { - pub id: i64, + pub id: SnowflakeId, pub name: String, pub slug: String, } @@ -323,7 +313,7 @@ pub async fn get_post_tags( Ok(rows .into_iter() .map(|r| TagBrief { - id: r.id, + id: r.id.to_string(), name: r.name, slug: r.slug, }) @@ -514,7 +504,7 @@ pub async fn find_all_joined( let result = raisfast_derive::crud_join_paged!( pool, PostJoinedRow, select: [ - "p.id", "p.document_id", "p.tenant_id", "p.title", "p.slug", + "p.id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", @@ -540,7 +530,7 @@ pub async fn find_all_joined( let result = raisfast_derive::crud_join_paged!( pool, PostJoinedRow, select: [ - "p.id", "p.document_id", "p.tenant_id", "p.title", "p.slug", + "p.id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", @@ -580,8 +570,7 @@ pub async fn find_all_joined( #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct PostJoinedRow { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub title: String, pub slug: String, @@ -589,9 +578,9 @@ pub struct PostJoinedRow { pub excerpt: Option, pub cover_image: Option, pub status: PostStatus, - pub created_by: i64, - pub updated_by: Option, - pub category_id: Option, + pub created_by: SnowflakeId, + pub updated_by: Option, + pub category_id: Option, pub view_count: i64, pub is_pinned: bool, pub password: Option, @@ -613,7 +602,7 @@ pub struct PostJoinedRow { } const JOIN_SQL: &str = "\ - SELECT p.id, p.document_id, p.tenant_id, p.title, p.slug, p.content, p.excerpt, p.cover_image, p.status, \ + SELECT p.id, p.tenant_id, p.title, p.slug, p.content, p.excerpt, p.cover_image, p.status, \ p.created_by, p.updated_by, p.category_id, p.view_count, p.is_pinned, \ p.password, p.comment_status, p.format, p.template, \ p.meta_title, p.meta_description, p.og_title, p.og_description, p.og_image, p.canonical_url, p.reading_time, \ @@ -630,7 +619,7 @@ pub async fn find_joined_by_id( ) -> AppResult { raisfast_derive::crud_join!( pool, PostJoinedRow, - select: ["p.id", "p.document_id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", "p.meta_title", "p.meta_description", "p.og_title", "p.og_description", "p.og_image", "p.canonical_url", "p.reading_time", "p.created_at", "p.updated_at", "p.published_at", "u.username AS author_name", "c.name AS category_name"], + select: ["p.id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", "p.meta_title", "p.meta_description", "p.og_title", "p.og_description", "p.og_image", "p.canonical_url", "p.reading_time", "p.created_at", "p.updated_at", "p.published_at", "u.username AS author_name", "c.name AS category_name"], from: "posts p", joins: [ LEFT "users u" ON "p.created_by = u.id", @@ -650,7 +639,7 @@ pub async fn find_published_joined_by_slug( ) -> AppResult { raisfast_derive::crud_join!( pool, PostJoinedRow, - select: ["p.id", "p.document_id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", "p.meta_title", "p.meta_description", "p.og_title", "p.og_description", "p.og_image", "p.canonical_url", "p.reading_time", "p.created_at", "p.updated_at", "p.published_at", "u.username AS author_name", "c.name AS category_name"], + select: ["p.id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", "p.meta_title", "p.meta_description", "p.og_title", "p.og_description", "p.og_image", "p.canonical_url", "p.reading_time", "p.created_at", "p.updated_at", "p.published_at", "u.username AS author_name", "c.name AS category_name"], from: "posts p", joins: [ LEFT "users u" ON "p.created_by = u.id", @@ -666,17 +655,17 @@ pub async fn find_published_joined_by_slug( pub async fn get_tags_for_posts( pool: &crate::db::Pool, - post_ids: &[i64], + post_ids: &[SnowflakeId], tenant_id: Option<&str>, -) -> AppResult>> { +) -> AppResult>> { if post_ids.is_empty() { return Ok(std::collections::HashMap::new()); } #[derive(Debug, FromRow)] struct TagWithPostId { - post_id: i64, - id: i64, + post_id: SnowflakeId, + id: SnowflakeId, name: String, slug: String, } @@ -693,10 +682,11 @@ pub async fn get_tags_for_posts( method: fetch_all )?; - let mut map: std::collections::HashMap> = std::collections::HashMap::new(); + let mut map: std::collections::HashMap> = + std::collections::HashMap::new(); for row in rows { map.entry(row.post_id).or_default().push(TagBrief { - id: row.id, + id: row.id.to_string(), name: row.name, slug: row.slug, }); @@ -717,7 +707,7 @@ pub async fn find_joined_by_ids( pool, PostJoinedRow, select: [ - "p.id", "p.document_id", "p.tenant_id", "p.title", "p.slug", + "p.id", "p.tenant_id", "p.title", "p.slug", "p.content", "p.excerpt", "p.cover_image", "p.status", "p.created_by", "p.updated_by", "p.category_id", "p.view_count", "p.is_pinned", "p.password", "p.comment_status", "p.format", "p.template", @@ -772,7 +762,6 @@ pub async fn find_published_joined( raisfast_derive::check_schema!( "posts", "id", - "document_id", "title", "slug", "content", @@ -954,21 +943,15 @@ mod tests { } async fn create_user(pool: &crate::db::Pool) -> i64 { - let uid = uuid::Uuid::now_v7().to_string(); + let uid = crate::utils::id::new_id(); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email')", ) - .bind(&uid) + .bind(uid) .execute(pool) .await .unwrap(); - - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&uid) - .fetch_one(pool) - .await - .unwrap(); - id + uid } async fn create_test_post( @@ -1009,7 +992,7 @@ mod tests { let pool = setup_pool().await; let uid = create_user(&pool).await; let p = create_test_post(&pool, uid, "published", "Test Post").await; - let result = find_joined_by_ids(&pool, &[p.id], None).await.unwrap(); + let result = find_joined_by_ids(&pool, &[*p.id], None).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].id, p.id); assert_eq!(result[0].title, "Test Post"); @@ -1023,14 +1006,14 @@ mod tests { let p1 = create_test_post(&pool, uid, "published", "Post A").await; let p2 = create_test_post(&pool, uid, "published", "Post B").await; let p3 = create_test_post(&pool, uid, "published", "Post C").await; - let result = find_joined_by_ids(&pool, &[p1.id, p3.id], None) + let result = find_joined_by_ids(&pool, &[*p1.id, *p3.id], None) .await .unwrap(); assert_eq!(result.len(), 2); - let ids: Vec = result.iter().map(|r| r.id).collect(); - assert!(ids.contains(&p1.id)); - assert!(ids.contains(&p3.id)); - assert!(!ids.contains(&p2.id)); + let ids: Vec = result.iter().map(|r| *r.id).collect(); + assert!(ids.contains(&*p1.id)); + assert!(ids.contains(&*p3.id)); + assert!(!ids.contains(&*p2.id)); } #[tokio::test] @@ -1038,7 +1021,7 @@ mod tests { let pool = setup_pool().await; let uid = create_user(&pool).await; let p = create_test_post(&pool, uid, "draft", "Draft Post").await; - let result = find_joined_by_ids(&pool, &[p.id], None).await.unwrap(); + let result = find_joined_by_ids(&pool, &[*p.id], None).await.unwrap(); assert!(result.is_empty()); } @@ -1055,7 +1038,7 @@ mod tests { let uid = create_user(&pool).await; let pub_post = create_test_post(&pool, uid, "published", "Published").await; let draft_post = create_test_post(&pool, uid, "draft", "Draft").await; - let result = find_joined_by_ids(&pool, &[pub_post.id, draft_post.id], None) + let result = find_joined_by_ids(&pool, &[*pub_post.id, *draft_post.id], None) .await .unwrap(); assert_eq!(result.len(), 1); @@ -1066,18 +1049,12 @@ mod tests { async fn find_joined_by_ids_with_category() { let pool = setup_pool().await; let uid = create_user(&pool).await; - let cat_doc_id = uuid::Uuid::now_v7().to_string(); - sqlx::query("INSERT INTO categories (document_id, name, slug) VALUES (?, 'Tech', 'tech')") - .bind(&cat_doc_id) + let cat_id = crate::utils::id::new_id(); + sqlx::query("INSERT INTO categories (id, name, slug) VALUES (?, 'Tech', 'tech')") + .bind(cat_id) .execute(&pool) .await .unwrap(); - let (cat_int_id,): (i64,) = - sqlx::query_as("SELECT id FROM categories WHERE document_id = ?") - .bind(&cat_doc_id) - .fetch_one(&pool) - .await - .unwrap(); let p = create( &pool, &CreatePostCmd { @@ -1089,14 +1066,14 @@ mod tests { status: PostStatus::Published, created_by: uid, updated_by: Some(uid), - category_id: Some(cat_int_id), + category_id: Some(cat_id), tag_ids: None, }, None, ) .await .unwrap(); - let result = find_joined_by_ids(&pool, &[p.id], None).await.unwrap(); + let result = find_joined_by_ids(&pool, &[*p.id], None).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].category_name.as_deref(), Some("Tech")); } @@ -1113,7 +1090,7 @@ mod tests { let pool = setup_pool().await; let uid = create_user(&pool).await; let p = create_test_post(&pool, uid, "published", "Count Post").await; - let count = count_published_by_ids(&pool, &[p.id], None).await.unwrap(); + let count = count_published_by_ids(&pool, &[*p.id], None).await.unwrap(); assert_eq!(count, 1); } @@ -1122,7 +1099,7 @@ mod tests { let pool = setup_pool().await; let uid = create_user(&pool).await; let p = create_test_post(&pool, uid, "draft", "Draft").await; - let count = count_published_by_ids(&pool, &[p.id], None).await.unwrap(); + let count = count_published_by_ids(&pool, &[*p.id], None).await.unwrap(); assert_eq!(count, 0); } @@ -1133,7 +1110,7 @@ mod tests { let p1 = create_test_post(&pool, uid, "published", "A").await; let p2 = create_test_post(&pool, uid, "draft", "B").await; let p3 = create_test_post(&pool, uid, "published", "C").await; - let count = count_published_by_ids(&pool, &[p1.id, p2.id, p3.id], None) + let count = count_published_by_ids(&pool, &[*p1.id, *p2.id, *p3.id], None) .await .unwrap(); assert_eq!(count, 2); diff --git a/src/models/product.rs b/src/models/product.rs index e7750ae5..7a23678b 100644 --- a/src/models/product.rs +++ b/src/models/product.rs @@ -4,6 +4,7 @@ use ts_rs::TS; use crate::commands::{CreateProductCmd, UpdateProductCmd}; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -36,10 +37,9 @@ define_enum!( #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Product { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub category_id: Option, + pub category_id: Option, pub title: String, pub description: Option, pub cover_url: Option, @@ -47,7 +47,7 @@ pub struct Product { pub fulfillment_type: FulfillmentType, pub delivery_hook: Option, pub weight: Option, - pub shipping_template_id: Option, + pub shipping_template_id: Option, pub price: i64, pub currency: String, pub status: ProductStatus, @@ -86,15 +86,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "products", Product, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn find_active_paginated( pool: &crate::db::Pool, tenant_id: Option<&str>, @@ -138,13 +129,12 @@ pub async fn insert( cmd: &CreateProductCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "products", [ - "document_id" => &document_id, + "id" => id, "category_id" => cmd.category_id, "title" => &cmd.title, "description" => &cmd.description, @@ -177,13 +167,9 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) + find_by_id(pool, id, tenant_id) .await? - .ok_or_else(|| { - AppError::Internal(anyhow::anyhow!( - "product not found after insert: {document_id}" - )) - }) + .ok_or_else(|| AppError::Internal(anyhow::anyhow!("product not found after insert"))) } pub async fn update( @@ -311,7 +297,10 @@ mod tests { async fn insert_and_find_by_id() { let pool = setup_pool().await; let p = seed_product(&pool, "Widget", "draft").await; - let found = super::find_by_id(&pool, p.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *p.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.id, p.id); assert_eq!(found.title, "Widget"); assert_eq!(found.price, 1000); @@ -319,17 +308,6 @@ mod tests { assert_eq!(found.version, 1); } - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let p = seed_product(&pool, "Gadget", "draft").await; - let found = super::find_by_document_id(&pool, &p.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, p.id); - } - #[tokio::test] async fn find_by_id_not_found() { let pool = setup_pool().await; @@ -341,17 +319,6 @@ mod tests { ); } - #[tokio::test] - async fn find_by_document_id_not_found() { - let pool = setup_pool().await; - assert!( - super::find_by_document_id(&pool, "nonexistent", None) - .await - .unwrap() - .is_none() - ); - } - #[tokio::test] async fn insert_sets_defaults() { let pool = setup_pool().await; @@ -402,11 +369,11 @@ mod tests { async fn update_changes_title_and_price() { let pool = setup_pool().await; let p = seed_product(&pool, "Old", "draft").await; - let version = get_version(&pool, p.id).await; + let version = get_version(&pool, *p.id).await; let ok = super::update( &pool, &UpdateProductCmd { - id: p.id, + id: *p.id, category_id: None, title: "New".to_string(), description: Some("desc".to_string()), @@ -444,7 +411,10 @@ mod tests { .await .unwrap(); assert!(ok); - let found = super::find_by_id(&pool, p.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *p.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.title, "New"); assert_eq!(found.price, 2000); assert_eq!(found.status, ProductStatus::Active); @@ -459,7 +429,7 @@ mod tests { let ok = super::update( &pool, &UpdateProductCmd { - id: p.id, + id: *p.id, category_id: None, title: "New".to_string(), description: None, @@ -503,10 +473,10 @@ mod tests { async fn delete_removes_product() { let pool = setup_pool().await; let p = seed_product(&pool, "Bye", "draft").await; - let ok = super::delete_by_id(&pool, p.id, None).await.unwrap(); + let ok = super::delete_by_id(&pool, *p.id, None).await.unwrap(); assert!(ok); assert!( - super::find_by_id(&pool, p.id, None) + super::find_by_id(&pool, *p.id, None) .await .unwrap() .is_none() @@ -525,10 +495,10 @@ mod tests { let pool = setup_pool().await; for i in 0..5 { let p = seed_product(&pool, &format!("P{i}"), "draft").await; - set_status(&pool, p.id, "active").await; + set_status(&pool, *p.id, "active").await; } let p = seed_product(&pool, "Draft", "draft").await; - set_status(&pool, p.id, "draft").await; + set_status(&pool, *p.id, "draft").await; let (items, total) = super::find_active_paginated(&pool, None, 1, 3) .await @@ -543,7 +513,7 @@ mod tests { let pool = setup_pool().await; for i in 0..5 { let p = seed_product(&pool, &format!("P{i}"), "draft").await; - set_status(&pool, p.id, "active").await; + set_status(&pool, *p.id, "active").await; } let (items, total) = super::find_active_paginated(&pool, None, 2, 3) .await @@ -570,7 +540,7 @@ mod tests { let pool = setup_pool().await; for i in 0..3 { let p = seed_product(&pool, &format!("Active{i}"), "draft").await; - set_status(&pool, p.id, "active").await; + set_status(&pool, *p.id, "active").await; } seed_product(&pool, "Draft1", "draft").await; diff --git a/src/models/product_variant.rs b/src/models/product_variant.rs index a691239b..1733b347 100644 --- a/src/models/product_variant.rs +++ b/src/models/product_variant.rs @@ -3,15 +3,15 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct ProductVariant { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub product_id: i64, + pub product_id: SnowflakeId, pub sku: Option, pub title: String, pub price: i64, @@ -39,21 +39,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!( - pool, - "product_variants", - ProductVariant, - "document_id" => document_id, - tenant: tenant_id - ) - .map_err(Into::into) -} - pub async fn find_by_sku( pool: &crate::db::Pool, sku: &str, @@ -105,13 +90,12 @@ pub async fn insert( cmd: &crate::commands::CreateProductVariantCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "product_variants", [ - "document_id" => &document_id, + "id" => id, "product_id" => cmd.product_id, "sku" => &cmd.sku, "title" => &cmd.title, @@ -126,13 +110,9 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) - .await? - .ok_or_else(|| { - AppError::Internal(anyhow::anyhow!( - "product_variant not found after insert: {document_id}" - )) - }) + find_by_id(pool, id, tenant_id).await?.ok_or_else(|| { + AppError::Internal(anyhow::anyhow!("product_variant not found after insert")) + }) } pub async fn update( @@ -280,7 +260,10 @@ mod tests { let pid = seed_product(&pool).await; let v = seed_variant(&pool, pid, "Red Shirt", "SKU-RED-001").await; - let found = super::find_by_id(&pool, v.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *v.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.id, v.id); assert_eq!(found.title, "Red Shirt"); assert_eq!(found.sku.unwrap(), "SKU-RED-001"); @@ -289,18 +272,6 @@ mod tests { assert!(found.is_active); } - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let pid = seed_product(&pool).await; - let v = seed_variant(&pool, pid, "Blue", "SKU-BLU").await; - let found = super::find_by_document_id(&pool, &v.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, v.id); - } - #[tokio::test] async fn find_by_sku() { let pool = setup_pool().await; @@ -380,7 +351,7 @@ mod tests { let ok = super::update( &pool, &crate::commands::UpdateProductVariantCmd { - id: v.id, + id: *v.id, sku: Some("SKU-NEW".to_string()), title: "New".to_string(), price: 2000, @@ -396,7 +367,10 @@ mod tests { .unwrap(); assert!(ok); - let found = super::find_by_id(&pool, v.id, None).await.unwrap().unwrap(); + let found = super::find_by_id(&pool, *v.id, None) + .await + .unwrap() + .unwrap(); assert_eq!(found.title, "New"); assert_eq!(found.sku.unwrap(), "SKU-NEW"); assert_eq!(found.price, 2000); @@ -432,10 +406,10 @@ mod tests { let pool = setup_pool().await; let pid = seed_product(&pool).await; let v = seed_variant(&pool, pid, "Bye", "SKU-BYE").await; - let ok = super::delete_by_id(&pool, v.id, None).await.unwrap(); + let ok = super::delete_by_id(&pool, *v.id, None).await.unwrap(); assert!(ok); assert!( - super::find_by_id(&pool, v.id, None) + super::find_by_id(&pool, *v.id, None) .await .unwrap() .is_none() diff --git a/src/models/rbac.rs b/src/models/rbac.rs index c16b0910..de9d2fdf 100644 --- a/src/models/rbac.rs +++ b/src/models/rbac.rs @@ -10,14 +10,14 @@ use ts_rs::TS; use crate::commands::CreatePermissionCmd; use crate::db::dialect::ph; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// Row model for the roles table #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct Role { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub name: String, pub description: Option, pub is_system: bool, @@ -29,9 +29,8 @@ pub struct Role { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct Permission { - pub id: i64, - pub document_id: String, - pub role_id: i64, + pub id: SnowflakeId, + pub role_id: SnowflakeId, pub action: String, pub subject: String, pub fields: Option, @@ -44,7 +43,6 @@ pub async fn list_roles(pool: &crate::db::Pool) -> AppResult> { raisfast_derive::check_schema!( "roles", "id", - "document_id", "name", "description", "is_system", @@ -55,9 +53,9 @@ pub async fn list_roles(pool: &crate::db::Pool) -> AppResult> { Ok(roles) } -/// Find role by document_id -pub async fn find_role_by_id(pool: &crate::db::Pool, document_id: &str) -> AppResult> { - let role = raisfast_derive::crud_find!(pool, "roles", Role, "document_id" => document_id)?; +/// Find role by id +pub async fn find_role_by_id(pool: &crate::db::Pool, id: i64) -> AppResult> { + let role = raisfast_derive::crud_find!(pool, "roles", Role, "id" => id)?; Ok(role) } @@ -76,16 +74,15 @@ pub async fn find_role_id_by_name(pool: &crate::db::Pool, name: &str) -> AppResu /// Create role pub async fn create_role( pool: &crate::db::Pool, - document_id: &str, name: &str, description: Option<&str>, ) -> AppResult { - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "roles", [ - "document_id" => document_id, + "id" => id, "name" => name, "description" => description, "is_system" => 0_i64, @@ -95,7 +92,7 @@ pub async fn create_role( ) .map_err(|e| AppError::Conflict(format!("create role failed: {e}")))?; - find_role_by_id(pool, document_id) + find_role_by_id(pool, id) .await? .ok_or_else(|| AppError::not_found("role")) } @@ -103,7 +100,7 @@ pub async fn create_role( /// Update role (dynamic SET clause) pub async fn update_role( pool: &crate::db::Pool, - document_id: &str, + id: i64, name: Option<&str>, description: Option<&str>, ) -> AppResult { @@ -112,17 +109,17 @@ pub async fn update_role( pool, "roles", bind: ["updated_at" => now], optional: ["name" => name, "description" => description], - where: "document_id" => document_id + where: "id" => id )?; - find_role_by_id(pool, document_id) + find_role_by_id(pool, id) .await? - .ok_or_else(|| AppError::not_found(&format!("role/{document_id}"))) + .ok_or_else(|| AppError::not_found(&format!("role/{id}"))) } /// Delete role -pub async fn delete_role(pool: &crate::db::Pool, document_id: &str) -> AppResult<()> { - raisfast_derive::crud_delete!(pool, "roles", "document_id" => document_id)?; +pub async fn delete_role(pool: &crate::db::Pool, id: i64) -> AppResult<()> { + raisfast_derive::crud_delete!(pool, "roles", "id" => id)?; Ok(()) } @@ -134,7 +131,6 @@ pub async fn find_permissions_by_role_id( raisfast_derive::check_schema!( "permissions", "id", - "document_id", "role_id", "action", "subject", @@ -154,9 +150,9 @@ pub async fn delete_permissions_by_role_id(pool: &crate::db::Pool, role_id: i64) /// Insert a single permission pub async fn insert_permission(pool: &crate::db::Pool, cmd: &CreatePermissionCmd) -> AppResult<()> { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "permissions", [ - "document_id" => &document_id, + "id" => id, "role_id" => cmd.role_id, "action" => &cmd.action, "subject" => &cmd.subject, @@ -178,14 +174,12 @@ mod tests { #[sqlx::test] async fn create_and_find_role_by_id() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); - let role = create_role(&pool, &doc_id, "admin_test", Some("desc")) + let role = create_role(&pool, "admin_test", Some("desc")) .await .unwrap(); - assert_eq!(role.document_id, doc_id); assert_eq!(role.name, "admin_test"); - let found = find_role_by_id(&pool, &doc_id).await.unwrap().unwrap(); + let found = find_role_by_id(&pool, *role.id).await.unwrap().unwrap(); assert_eq!(found.id, role.id); } @@ -193,8 +187,7 @@ mod tests { async fn list_roles_test() { let pool = setup_pool().await; for i in 0..3 { - let doc_id = crate::utils::id::new_document_id(); - create_role(&pool, &doc_id, &format!("role_{i}"), None) + create_role(&pool, &format!("role_{i}"), None) .await .unwrap(); } @@ -205,10 +198,9 @@ mod tests { #[sqlx::test] async fn update_role_changes_name() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); - create_role(&pool, &doc_id, "original", None).await.unwrap(); + let role = create_role(&pool, "original", None).await.unwrap(); - let updated = update_role(&pool, &doc_id, Some("new_name"), None) + let updated = update_role(&pool, *role.id, Some("new_name"), None) .await .unwrap(); assert_eq!(updated.name, "new_name"); @@ -217,28 +209,22 @@ mod tests { #[sqlx::test] async fn delete_role_test() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); - create_role(&pool, &doc_id, "to_delete", None) - .await - .unwrap(); + let role = create_role(&pool, "to_delete", None).await.unwrap(); - super::delete_role(&pool, &doc_id).await.unwrap(); - let found = find_role_by_id(&pool, &doc_id).await.unwrap(); + super::delete_role(&pool, *role.id).await.unwrap(); + let found = find_role_by_id(&pool, *role.id).await.unwrap(); assert!(found.is_none()); } #[sqlx::test] async fn permissions_crud() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); - let role = create_role(&pool, &doc_id, "perm_role", None) - .await - .unwrap(); + let role = create_role(&pool, "perm_role", None).await.unwrap(); insert_permission( &pool, &CreatePermissionCmd { - role_id: role.id, + role_id: *role.id, action: "read".to_string(), subject: "posts".to_string(), fields: None, @@ -250,7 +236,7 @@ mod tests { insert_permission( &pool, &CreatePermissionCmd { - role_id: role.id, + role_id: *role.id, action: "write".to_string(), subject: "posts".to_string(), fields: None, @@ -260,26 +246,25 @@ mod tests { .await .unwrap(); - let perms = find_permissions_by_role_id(&pool, role.id).await.unwrap(); + let perms = find_permissions_by_role_id(&pool, *role.id).await.unwrap(); assert_eq!(perms.len(), 2); - delete_permissions_by_role_id(&pool, role.id).await.unwrap(); - let perms = find_permissions_by_role_id(&pool, role.id).await.unwrap(); + delete_permissions_by_role_id(&pool, *role.id) + .await + .unwrap(); + let perms = find_permissions_by_role_id(&pool, *role.id).await.unwrap(); assert!(perms.is_empty()); } #[sqlx::test] async fn find_role_id_by_name_test() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); - let role = create_role(&pool, &doc_id, "lookup_name", None) - .await - .unwrap(); + let role = create_role(&pool, "lookup_name", None).await.unwrap(); let id = super::find_role_id_by_name(&pool, "lookup_name") .await .unwrap() .unwrap(); - assert_eq!(id, role.id); + assert_eq!(id, *role.id); } } diff --git a/src/models/refresh_token.rs b/src/models/refresh_token.rs index 61122b79..537180d6 100644 --- a/src/models/refresh_token.rs +++ b/src/models/refresh_token.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// Refresh token full database row model @@ -16,9 +17,8 @@ use crate::utils::tz::Timestamp; /// `expires_at` is the expiration time in ISO 8601 format. #[derive(Debug, FromRow, Serialize, Deserialize)] pub struct RefreshToken { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub token: String, pub expires_at: Timestamp, pub created_at: Timestamp, @@ -26,16 +26,16 @@ pub struct RefreshToken { /// Create a new refresh token record /// -/// Automatically generates a UUID v7 as the document_id. +/// Automatically generates a Snowflake ID as the primary key. pub async fn create_token( pool: &crate::db::Pool, user_id: i64, token: &str, expires_at: &str, ) -> AppResult<()> { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "refresh_tokens", [ - "document_id" => document_id, + "id" => id, "user_id" => user_id, "token" => token, "expires_at" => expires_at, @@ -78,18 +78,18 @@ mod tests { async fn insert_user(pool: &crate::db::Pool) -> i64 { let cmd = crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }; let user = crate::models::user::create(pool, &cmd, None).await.unwrap(); - user.id + *user.id } #[tokio::test] async fn create_and_find_by_token() { let pool = setup_pool().await; let user_id = insert_user(&pool).await; - let token = crate::utils::id::new_document_id(); + let token = crate::utils::id::new_id().to_string(); create_token(&pool, user_id, &token, "2099-12-31T00:00:00Z") .await .unwrap(); @@ -106,7 +106,7 @@ mod tests { async fn delete_by_token() { let pool = setup_pool().await; let user_id = insert_user(&pool).await; - let token = crate::utils::id::new_document_id(); + let token = crate::utils::id::new_id().to_string(); create_token(&pool, user_id, &token, "2099-12-31T00:00:00Z") .await .unwrap(); @@ -118,8 +118,8 @@ mod tests { async fn delete_by_user() { let pool = setup_pool().await; let user_id = insert_user(&pool).await; - let token1 = crate::utils::id::new_document_id(); - let token2 = crate::utils::id::new_document_id(); + let token1 = crate::utils::id::new_id().to_string(); + let token2 = crate::utils::id::new_id().to_string(); create_token(&pool, user_id, &token1, "2099-12-31T00:00:00Z") .await .unwrap(); diff --git a/src/models/reusable_block.rs b/src/models/reusable_block.rs index b3d1e719..e3077ff1 100644 --- a/src/models/reusable_block.rs +++ b/src/models/reusable_block.rs @@ -7,20 +7,20 @@ use ts_rs::TS; use crate::commands::{CreateReusableBlockCmd, UpdateReusableBlockCmd}; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct ReusableBlock { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub name: String, pub block_type: String, pub content: String, pub description: Option, - pub created_by: Option, - pub updated_by: Option, + pub created_by: Option, + pub updated_by: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -35,16 +35,6 @@ pub async fn find_reusable_by_id( ) } -pub async fn find_reusable_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - Ok( - raisfast_derive::crud_find!(pool, "reusable_blocks", ReusableBlock, "document_id" => document_id, tenant: tenant_id)?, - ) -} - pub async fn list_reusable( pool: &crate::db::Pool, tenant_id: Option<&str>, @@ -57,12 +47,12 @@ pub async fn create_reusable( cmd: &CreateReusableBlockCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "reusable_blocks", [ - "document_id" => &document_id, + "id" => id, "name" => &cmd.name, "block_type" => &cmd.block_type, "content" => &cmd.content, @@ -75,7 +65,7 @@ pub async fn create_reusable( tenant: tenant_id )?; - find_reusable_by_document_id(pool, &document_id, tenant_id) + find_reusable_by_id(pool, id, tenant_id) .await? .ok_or_else(|| AppError::not_found("reusable_block")) } @@ -128,7 +118,7 @@ mod tests { ) .await .unwrap(); - user.id + *user.id } #[sqlx::test] @@ -148,35 +138,11 @@ mod tests { ) .await .unwrap(); - let found = find_reusable_by_id(&pool, block.id, None).await.unwrap(); + let found = find_reusable_by_id(&pool, *block.id, None).await.unwrap(); assert!(found.is_some()); assert_eq!(found.unwrap().name, "Block"); } - #[sqlx::test] - async fn find_by_document_id_test() { - let pool = setup_pool().await; - let uid = insert_user(&pool).await; - let block = create_reusable( - &pool, - &CreateReusableBlockCmd { - name: "Block".to_string(), - block_type: "text".to_string(), - content: "[]".to_string(), - description: None, - created_by: Some(uid), - }, - None, - ) - .await - .unwrap(); - let found = super::find_reusable_by_document_id(&pool, &block.document_id, None) - .await - .unwrap(); - assert!(found.is_some()); - assert_eq!(found.unwrap().id, block.id); - } - #[sqlx::test] async fn list_reusable_test() { let pool = setup_pool().await; @@ -220,7 +186,7 @@ mod tests { let updated = update_reusable( &pool, &UpdateReusableBlockCmd { - id: block.id, + id: *block.id, name: Some("Updated".to_string()), block_type: None, content: None, @@ -251,8 +217,8 @@ mod tests { ) .await .unwrap(); - delete_reusable(&pool, block.id, None).await.unwrap(); - let found = find_reusable_by_id(&pool, block.id, None).await.unwrap(); + delete_reusable(&pool, *block.id, None).await.unwrap(); + let found = find_reusable_by_id(&pool, *block.id, None).await.unwrap(); assert!(found.is_none()); } } diff --git a/src/models/sms_code.rs b/src/models/sms_code.rs index 7d5eb6ed..43376bd3 100644 --- a/src/models/sms_code.rs +++ b/src/models/sms_code.rs @@ -5,14 +5,14 @@ use sqlx::FromRow; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; use crate::utils::id; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; /// SMS verification code database row model #[derive(Debug, FromRow)] #[non_exhaustive] pub struct SmsCode { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub phone: String, pub code: String, pub purpose: String, @@ -49,12 +49,12 @@ pub async fn create( expires_in_secs: u64, ip_address: Option<&str>, ) -> AppResult { - let (document_id, now) = id::new_document_id_and_timestamp(); + let (id, now) = id::new_id_and_timestamp(); let expires_at = crate::utils::tz::now_utc() + chrono::Duration::seconds(expires_in_secs as i64); raisfast_derive::crud_insert!(pool, "sms_codes", [ - "document_id" => &document_id, + "id" => id, "phone" => phone, "code" => code, "purpose" => purpose, @@ -63,12 +63,9 @@ pub async fn create( "created_at" => now ])?; - raisfast_derive::crud_find!(pool, "sms_codes", SmsCode, "document_id" => &document_id)? - .ok_or_else(|| { - crate::errors::app_error::AppError::Internal(anyhow::anyhow!( - "failed to fetch sms code" - )) - }) + raisfast_derive::crud_find!(pool, "sms_codes", SmsCode, "id" => id)?.ok_or_else(|| { + crate::errors::app_error::AppError::Internal(anyhow::anyhow!("failed to fetch sms code")) + }) } /// Find a verification code by ID @@ -177,8 +174,9 @@ mod tests { } fn unique_phone() -> String { - let id = crate::utils::id::new_document_id(); + let id = crate::utils::id::new_id(); let hash = id + .to_string() .bytes() .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)); format!("1380000{:04}", hash % 10000) @@ -201,7 +199,7 @@ mod tests { .await .unwrap(); - let found = super::find_by_id(&pool, sms.id).await.unwrap(); + let found = super::find_by_id(&pool, *sms.id).await.unwrap(); assert!(found.is_some()); let row = found.unwrap(); assert_eq!(row.phone, phone); @@ -239,7 +237,7 @@ mod tests { .await .unwrap(); - let result = super::verify_code(&pool, sms.id, code).await.unwrap(); + let result = super::verify_code(&pool, *sms.id, code).await.unwrap(); assert_eq!(result, super::VerifyResult::Verified); } @@ -252,7 +250,7 @@ mod tests { .await .unwrap(); - let result = super::verify_code(&pool, sms.id, "000000").await.unwrap(); + let result = super::verify_code(&pool, *sms.id, "000000").await.unwrap(); assert_eq!(result, super::VerifyResult::WrongCode); } diff --git a/src/models/tag.rs b/src/models/tag.rs index 1bed7ceb..fab9daa5 100644 --- a/src/models/tag.rs +++ b/src/models/tag.rs @@ -8,13 +8,13 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct Tag { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub name: String, pub slug: String, @@ -25,8 +25,8 @@ pub struct Tag { pub og_title: Option, pub og_description: Option, pub og_image: Option, - pub created_by: Option, - pub updated_by: Option, + pub created_by: Option, + pub updated_by: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -63,15 +63,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult { - raisfast_derive::crud_find_one!(pool, "tags", Tag, "document_id" => document_id, tenant: tenant_id) - .map_err(Into::into) -} - pub async fn create( pool: &crate::db::Pool, name: &str, @@ -79,13 +70,13 @@ pub async fn create( tenant_id: Option<&str>, created_by: Option, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "tags", [ - "document_id" => &document_id, + "id" => id, "name" => name, "slug" => slug, "created_by" => created_by, @@ -96,12 +87,7 @@ pub async fn create( tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id).await -} - -pub async fn delete(pool: &crate::db::Pool, id: i64, tenant_id: Option<&str>) -> AppResult<()> { - let result = raisfast_derive::crud_delete!(pool, "tags", "id" => id, tenant: tenant_id)?; - AppError::expect_affected(&result, "tag") + find_by_id(pool, id, tenant_id).await } pub async fn update( @@ -121,6 +107,11 @@ pub async fn update( find_by_id(pool, id, tenant_id).await } +pub async fn delete(pool: &crate::db::Pool, id: i64, tenant_id: Option<&str>) -> AppResult<()> { + let result = raisfast_derive::crud_delete!(pool, "tags", "id" => id, tenant: tenant_id)?; + AppError::expect_affected(&result, "tag") +} + #[cfg(test)] mod tests { use super::*; @@ -136,23 +127,11 @@ mod tests { assert_eq!(tag.name, "rust"); assert_eq!(tag.slug, "rust"); - let found = find_by_id(&pool, tag.id, None).await.unwrap(); + let found = find_by_id(&pool, *tag.id, None).await.unwrap(); assert_eq!(found.id, tag.id); assert_eq!(found.name, "rust"); } - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let tag = create(&pool, "rust", "rust", None, None).await.unwrap(); - - let found = super::find_by_document_id(&pool, &tag.document_id, None) - .await - .unwrap(); - assert_eq!(found.id, tag.id); - assert_eq!(found.document_id, tag.document_id); - } - #[tokio::test] async fn find_all_returns_all() { let pool = setup_pool().await; @@ -183,7 +162,7 @@ mod tests { let pool = setup_pool().await; let tag = create(&pool, "rust", "rust", None, None).await.unwrap(); - let updated = update(&pool, tag.id, "Rust Lang", "rust-lang", None) + let updated = update(&pool, *tag.id, "Rust Lang", "rust-lang", None) .await .unwrap(); assert_eq!(updated.name, "Rust Lang"); @@ -196,8 +175,8 @@ mod tests { let pool = setup_pool().await; let tag = create(&pool, "rust", "rust", None, None).await.unwrap(); - delete(&pool, tag.id, None).await.unwrap(); - let result = find_by_id(&pool, tag.id, None).await; + delete(&pool, *tag.id, None).await.unwrap(); + let result = find_by_id(&pool, *tag.id, None).await; assert!(result.is_err()); } } diff --git a/src/models/tenant.rs b/src/models/tenant.rs index ffedde98..9f11e668 100644 --- a/src/models/tenant.rs +++ b/src/models/tenant.rs @@ -8,6 +8,7 @@ use sqlx::FromRow; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -21,8 +22,7 @@ define_enum!( #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct Tenant { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub name: String, pub domain: Option, pub config: String, @@ -36,7 +36,6 @@ pub async fn find_all(pool: &crate::db::Pool) -> AppResult> { raisfast_derive::check_schema!( "tenants", "id", - "document_id", "name", "domain", "config", @@ -48,10 +47,9 @@ pub async fn find_all(pool: &crate::db::Pool) -> AppResult> { Ok(tenants) } -/// Find a tenant by document_id -pub async fn find_by_id(pool: &crate::db::Pool, document_id: &str) -> AppResult> { - let tenant = - raisfast_derive::crud_find!(pool, "tenants", Tenant, "document_id" => document_id)?; +/// Find a tenant by integer primary key +pub async fn find_by_id(pool: &crate::db::Pool, id: i64) -> AppResult> { + let tenant = raisfast_derive::crud_find!(pool, "tenants", Tenant, "id" => id)?; Ok(tenant) } @@ -64,17 +62,16 @@ pub async fn find_by_domain(pool: &crate::db::Pool, domain: &str) -> AppResult, config: &str, ) -> AppResult { - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "tenants", [ - "document_id" => document_id, + "id" => id, "name" => name, "domain" => domain, "config" => config, @@ -85,7 +82,7 @@ pub async fn create( ) .map_err(|e| AppError::Conflict(format!("create tenant failed: {e}")))?; - find_by_id(pool, document_id) + find_by_id(pool, id) .await? .ok_or_else(|| AppError::not_found("tenant")) } @@ -93,7 +90,7 @@ pub async fn create( /// Update a tenant pub async fn update( pool: &crate::db::Pool, - document_id: &str, + id: i64, name: Option<&str>, domain: Option<&str>, config: Option<&str>, @@ -104,17 +101,17 @@ pub async fn update( pool, "tenants", bind: ["updated_at" => now], optional: ["name" => name, "domain" => domain, "config" => config, "status" => status], - where: "document_id" => document_id + where: "id" => id )?; - find_by_id(pool, document_id) + find_by_id(pool, id) .await? - .ok_or_else(|| AppError::not_found(&format!("tenant/{document_id}"))) + .ok_or_else(|| AppError::not_found(&format!("tenant/{id}"))) } /// Delete a tenant -pub async fn delete(pool: &crate::db::Pool, document_id: &str) -> AppResult<()> { - raisfast_derive::crud_delete!(pool, "tenants", "document_id" => document_id)?; +pub async fn delete(pool: &crate::db::Pool, id: i64) -> AppResult<()> { + raisfast_derive::crud_delete!(pool, "tenants", "id" => id)?; Ok(()) } @@ -134,37 +131,27 @@ mod tests { #[tokio::test] async fn create_and_find_by_id() { let pool = setup_pool().await; - let doc_id = "tenant-001"; - let row = create(&pool, doc_id, "Test Tenant", Some("test.example.com"), "{}") + let row = create(&pool, "Test Tenant", Some("test.example.com"), "{}") .await .unwrap(); - assert_eq!(row.document_id, doc_id); assert_eq!(row.name, "Test Tenant"); assert_eq!(row.domain.unwrap(), "test.example.com"); - let found = find_by_id(&pool, doc_id).await.unwrap().unwrap(); + let found = find_by_id(&pool, *row.id).await.unwrap().unwrap(); assert_eq!(found.id, row.id); - assert_eq!(found.document_id, doc_id); } #[tokio::test] async fn find_by_domain_returns_match() { let pool = setup_pool().await; - create( - &pool, - "tenant-002", - "Dom Tenant", - Some("dom.example.com"), - "{}", - ) - .await - .unwrap(); + create(&pool, "Dom Tenant", Some("dom.example.com"), "{}") + .await + .unwrap(); let found = find_by_domain(&pool, "dom.example.com") .await .unwrap() .unwrap(); - assert_eq!(found.document_id, "tenant-002"); assert_eq!(found.name, "Dom Tenant"); let missing = find_by_domain(&pool, "no.such.domain").await.unwrap(); @@ -174,15 +161,9 @@ mod tests { #[tokio::test] async fn find_all_returns_all() { let pool = setup_pool().await; - create(&pool, "tenant-a", "Alpha", None, "{}") - .await - .unwrap(); - create(&pool, "tenant-b", "Bravo", None, "{}") - .await - .unwrap(); - create(&pool, "tenant-c", "Charlie", None, "{}") - .await - .unwrap(); + create(&pool, "Alpha", None, "{}").await.unwrap(); + create(&pool, "Bravo", None, "{}").await.unwrap(); + create(&pool, "Charlie", None, "{}").await.unwrap(); let all = find_all(&pool).await.unwrap(); assert!(all.len() >= 3); @@ -191,12 +172,11 @@ mod tests { #[tokio::test] async fn update_changes_name() { let pool = setup_pool().await; - let doc_id = "tenant-003"; - create(&pool, doc_id, "Original", Some("orig.example.com"), "{}") + let row = create(&pool, "Original", Some("orig.example.com"), "{}") .await .unwrap(); - let updated = update(&pool, doc_id, Some("Updated Name"), None, None, None) + let updated = update(&pool, *row.id, Some("Updated Name"), None, None, None) .await .unwrap(); assert_eq!(updated.name, "Updated Name"); @@ -206,11 +186,10 @@ mod tests { #[tokio::test] async fn delete_removes_tenant() { let pool = setup_pool().await; - let doc_id = "tenant-004"; - create(&pool, doc_id, "ToDelete", None, "{}").await.unwrap(); + let row = create(&pool, "ToDelete", None, "{}").await.unwrap(); - delete(&pool, doc_id).await.unwrap(); - let found = find_by_id(&pool, doc_id).await.unwrap(); + delete(&pool, *row.id).await.unwrap(); + let found = find_by_id(&pool, *row.id).await.unwrap(); assert!(found.is_none()); } } diff --git a/src/models/user.rs b/src/models/user.rs index 142ffcf8..4cc9f590 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; pub type SocialLinks = HashMap; @@ -41,8 +42,7 @@ define_enum!( #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] #[non_exhaustive] pub struct User { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub username: String, pub role: UserRole, @@ -84,19 +84,8 @@ pub async fn find_by_username(pool: &crate::db::Pool, username: &str) -> AppResu Ok(raisfast_derive::crud_find!(pool, "users", User, "username" => username)?) } -/// Find user by document_id (external API) -pub async fn find_by_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - Ok( - raisfast_derive::crud_find!(pool, "users", User, "document_id" => document_id, tenant: tenant_id)?, - ) -} - /// Find user by integer primary key (internal FK lookup) -pub async fn find_by_pk( +pub async fn find_by_id( pool: &crate::db::Pool, id: i64, tenant_id: Option<&str>, @@ -110,13 +99,13 @@ pub async fn create( cmd: &crate::commands::CreateUserCmd, tenant_id: Option<&str>, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "users", [ - "document_id" => &document_id, + "id" => id, "username" => &cmd.username, "created_at" => now, "updated_at" => now, @@ -127,7 +116,7 @@ pub async fn create( tenant: tenant_id )?; - let user = raisfast_derive::crud_find!(pool, "users", User, "document_id" => &document_id, tenant: tenant_id)? + let user = raisfast_derive::crud_find!(pool, "users", User, "id" => id, tenant: tenant_id)? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch newly created user")))?; Ok(user) } @@ -138,7 +127,7 @@ pub async fn update_profile( cmd: &crate::commands::UpdateProfileCmd, tenant_id: Option<&str>, ) -> AppResult { - let user = find_by_pk(pool, cmd.id, tenant_id) + let user = find_by_id(pool, cmd.id, tenant_id) .await? .ok_or_else(|| AppError::not_found("user"))?; let username = cmd.username.as_deref().unwrap_or(&user.username); @@ -173,7 +162,7 @@ pub async fn update_profile( where: "id" => user.id, tenant: tenant_id )?; - find_by_pk(pool, cmd.id, tenant_id) + find_by_id(pool, cmd.id, tenant_id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch updated user"))) } @@ -200,28 +189,28 @@ pub async fn find_all( /// Admin updates user role pub async fn update_role( pool: &crate::db::Pool, - document_id: &str, + id: i64, role: UserRole, tenant_id: Option<&str>, ) -> AppResult { let now = crate::utils::tz::now_utc(); let result = raisfast_derive::crud_update!(pool, "users", bind: ["role" => role, "updated_at" => &now], - where: "document_id" => document_id, + where: "id" => id, tenant: tenant_id )?; AppError::expect_affected(&result, "user")?; - find_by_id(pool, document_id, tenant_id) + find_by_id(pool, id, tenant_id) .await? .ok_or_else(|| AppError::not_found("user")) } -pub async fn delete_by_document_id( +pub async fn delete_by_id( pool: &crate::db::Pool, - document_id: &str, + id: i64, tenant_id: Option<&str>, ) -> AppResult<()> { - raisfast_derive::crud_delete!(pool, "users", "document_id" => document_id, tenant: tenant_id)?; + raisfast_derive::crud_delete!(pool, "users", "id" => id, tenant: tenant_id)?; Ok(()) } @@ -240,31 +229,21 @@ mod tests { #[tokio::test] async fn find_by_id() { let pool = setup_pool().await; - let user = create(&pool, &new_cmd("iduser"), None).await.unwrap(); - let found = super::find_by_id(&pool, &user.document_id, None) + let user = create(&pool, &new_cmd("pkuser"), None).await.unwrap(); + let found = super::find_by_id(&pool, *user.id, None) .await .unwrap() .unwrap(); assert_eq!(found.id, user.id); } #[tokio::test] - async fn find_by_pk() { - let pool = setup_pool().await; - let user = create(&pool, &new_cmd("pkuser"), None).await.unwrap(); - let found = super::find_by_pk(&pool, user.id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.document_id, user.document_id); - } - #[tokio::test] async fn update_profile() { let pool = setup_pool().await; let user = create(&pool, &new_cmd("profuser"), None).await.unwrap(); let updated = super::update_profile( &pool, &crate::commands::user::UpdateProfileCmd { - id: user.id, + id: *user.id, username: Some("newname".to_string()), bio: Some("hello world".to_string()), website: None, @@ -295,7 +274,7 @@ mod tests { let pool = setup_pool().await; let user = create(&pool, &new_cmd("roleuser"), None).await.unwrap(); assert_eq!(user.role, UserRole::Reader); - let updated = super::update_role(&pool, &user.document_id, UserRole::Author, None) + let updated = super::update_role(&pool, *user.id, UserRole::Author, None) .await .unwrap(); assert_eq!(updated.role, UserRole::Author); diff --git a/src/models/user_address.rs b/src/models/user_address.rs index 55799d75..9ae6a9b5 100644 --- a/src/models/user_address.rs +++ b/src/models/user_address.rs @@ -3,15 +3,15 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct UserAddress { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, - pub user_id: i64, + pub user_id: SnowflakeId, pub label: String, pub recipient_name: String, pub phone: String, @@ -43,21 +43,6 @@ pub async fn find_by_id( .map_err(Into::into) } -pub async fn find_by_document_id( - pool: &crate::db::Pool, - document_id: &str, - tenant_id: Option<&str>, -) -> AppResult> { - raisfast_derive::crud_find!( - pool, - "user_addresses", - UserAddress, - "document_id" => document_id, - tenant: tenant_id - ) - .map_err(Into::into) -} - pub async fn find_by_user_id( pool: &crate::db::Pool, user_id: i64, @@ -94,13 +79,12 @@ pub async fn insert( cmd: &crate::commands::CreateUserAddressCmd, tenant_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = crate::utils::tz::now_utc(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!( pool, "user_addresses", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "label" => &cmd.label, "recipient_name" => &cmd.recipient_name, @@ -119,13 +103,9 @@ pub async fn insert( ], tenant: tenant_id )?; - find_by_document_id(pool, &document_id, tenant_id) + find_by_id(pool, id, tenant_id) .await? - .ok_or_else(|| { - AppError::Internal(anyhow::anyhow!( - "user_address not found after insert: {document_id}" - )) - }) + .ok_or_else(|| AppError::Internal(anyhow::anyhow!("user_address not found after insert"))) } pub async fn update( @@ -192,21 +172,16 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", ) - .bind(&doc_id) + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -240,7 +215,7 @@ mod tests { let uid = seed_user(&pool).await; let addr = seed_address(&pool, uid, "Home").await; - let found = super::find_by_id(&pool, addr.id, None) + let found = super::find_by_id(&pool, *addr.id, None) .await .unwrap() .unwrap(); @@ -251,18 +226,6 @@ mod tests { assert_eq!(found.address_line1, "123 Main St"); } - #[tokio::test] - async fn find_by_document_id() { - let pool = setup_pool().await; - let uid = seed_user(&pool).await; - let addr = seed_address(&pool, uid, "Office").await; - let found = super::find_by_document_id(&pool, &addr.document_id, None) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, addr.id); - } - #[tokio::test] async fn find_by_user_id() { let pool = setup_pool().await; @@ -326,7 +289,7 @@ mod tests { let ok = super::update( &pool, &crate::commands::UpdateUserAddressCmd { - id: addr.id, + id: *addr.id, user_id: uid, label: "Updated".to_string(), recipient_name: "Jane".to_string(), @@ -347,7 +310,7 @@ mod tests { .unwrap(); assert!(ok); - let found = super::find_by_id(&pool, addr.id, None) + let found = super::find_by_id(&pool, *addr.id, None) .await .unwrap() .unwrap(); @@ -368,7 +331,7 @@ mod tests { let ok = super::update( &pool, &crate::commands::UpdateUserAddressCmd { - id: addr.id, + id: *addr.id, user_id: uid2, label: "Hacked".to_string(), recipient_name: "X".to_string(), @@ -396,12 +359,12 @@ mod tests { let uid = seed_user(&pool).await; let addr = seed_address(&pool, uid, "Bye").await; - let ok = super::delete_by_id(&pool, addr.id, uid, None) + let ok = super::delete_by_id(&pool, *addr.id, uid, None) .await .unwrap(); assert!(ok); assert!( - super::find_by_id(&pool, addr.id, None) + super::find_by_id(&pool, *addr.id, None) .await .unwrap() .is_none() @@ -415,7 +378,7 @@ mod tests { let uid2 = seed_user(&pool).await; let addr = seed_address(&pool, uid1, "Home").await; - let ok = super::delete_by_id(&pool, addr.id, uid2, None) + let ok = super::delete_by_id(&pool, *addr.id, uid2, None) .await .unwrap(); assert!(!ok); diff --git a/src/models/user_credential.rs b/src/models/user_credential.rs index d8828fd6..ae92d80b 100644 --- a/src/models/user_credential.rs +++ b/src/models/user_credential.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -39,9 +40,8 @@ pub fn extract_password_hash(credential_data: &str) -> AppResult { #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct UserCredential { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub auth_type: AuthType, pub identifier: String, pub credential_data: String, @@ -79,9 +79,9 @@ pub async fn create( credential_data: &str, verified: bool, ) -> AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "user_credentials", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "auth_type" => auth_type, "identifier" => identifier, @@ -90,7 +90,8 @@ pub async fn create( "created_at" => now, "updated_at" => now ])?; - let cred = raisfast_derive::crud_find_one!(pool, "user_credentials", UserCredential, "document_id" => &document_id)?; + let cred = + raisfast_derive::crud_find_one!(pool, "user_credentials", UserCredential, "id" => id)?; Ok(cred) } diff --git a/src/models/wallet.rs b/src/models/wallet.rs index d2b3ce76..adf03509 100644 --- a/src/models/wallet.rs +++ b/src/models/wallet.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -13,9 +14,8 @@ define_enum!( #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct Wallet { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub currency: String, pub balance: i64, pub version: i64, @@ -43,16 +43,15 @@ pub async fn find_by_user(pool: &crate::db::Pool, user_id: i64) -> AppResult AppResult { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); raisfast_derive::crud_insert!(pool, "wallets", [ - "document_id" => &document_id, + "id" => id, "user_id" => user_id, "currency" => currency, "created_at" => now, "updated_at" => now ])?; - raisfast_derive::crud_find_one!(pool, "wallets", Wallet, "document_id" => &document_id) - .map_err(Into::into) + raisfast_derive::crud_find_one!(pool, "wallets", Wallet, "id" => id).map_err(Into::into) } pub async fn find_or_create( @@ -97,7 +96,7 @@ mod tests { crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, @@ -110,7 +109,7 @@ mod tests { async fn create_wallet() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let w = create(&pool, user.id, "CNY").await.unwrap(); + let w = create(&pool, *user.id, "CNY").await.unwrap(); assert_eq!(w.user_id, user.id); assert_eq!(w.currency, "CNY"); assert_eq!(w.balance, 0); @@ -122,8 +121,8 @@ mod tests { async fn create_wallet_same_user_different_currency() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let w1 = create(&pool, user.id, "CNY").await.unwrap(); - let w2 = create(&pool, user.id, "USD").await.unwrap(); + let w1 = create(&pool, *user.id, "CNY").await.unwrap(); + let w2 = create(&pool, *user.id, "USD").await.unwrap(); assert_ne!(w1.id, w2.id); assert_eq!(w2.currency, "USD"); } @@ -132,9 +131,9 @@ mod tests { async fn find_by_id_found() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let w = create(&pool, user.id, "CNY").await.unwrap(); - let found = find_by_id(&pool, w.id).await.unwrap().unwrap(); - assert_eq!(found.document_id, w.document_id); + let w = create(&pool, *user.id, "CNY").await.unwrap(); + let found = find_by_id(&pool, *w.id).await.unwrap().unwrap(); + assert_eq!(found.id, w.id); } #[tokio::test] @@ -147,8 +146,8 @@ mod tests { async fn find_by_user_and_currency_found() { let pool = setup_pool().await; let user = insert_user(&pool).await; - create(&pool, user.id, "CNY").await.unwrap(); - let found = find_by_user_and_currency(&pool, user.id, "CNY") + create(&pool, *user.id, "CNY").await.unwrap(); + let found = find_by_user_and_currency(&pool, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -159,9 +158,9 @@ mod tests { async fn find_by_user_and_currency_wrong_currency() { let pool = setup_pool().await; let user = insert_user(&pool).await; - create(&pool, user.id, "CNY").await.unwrap(); + create(&pool, *user.id, "CNY").await.unwrap(); assert!( - find_by_user_and_currency(&pool, user.id, "USD") + find_by_user_and_currency(&pool, *user.id, "USD") .await .unwrap() .is_none() @@ -172,9 +171,9 @@ mod tests { async fn find_by_user_returns_all_wallets() { let pool = setup_pool().await; let user = insert_user(&pool).await; - create(&pool, user.id, "CNY").await.unwrap(); - create(&pool, user.id, "USD").await.unwrap(); - let wallets = find_by_user(&pool, user.id).await.unwrap(); + create(&pool, *user.id, "CNY").await.unwrap(); + create(&pool, *user.id, "USD").await.unwrap(); + let wallets = find_by_user(&pool, *user.id).await.unwrap(); assert_eq!(wallets.len(), 2); } @@ -189,7 +188,7 @@ mod tests { async fn find_or_create_creates_new() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let w = find_or_create(&pool, user.id, "CNY").await.unwrap(); + let w = find_or_create(&pool, *user.id, "CNY").await.unwrap(); assert_eq!(w.currency, "CNY"); assert_eq!(w.balance, 0); } @@ -198,8 +197,8 @@ mod tests { async fn find_or_create_returns_existing() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let w1 = find_or_create(&pool, user.id, "CNY").await.unwrap(); - let w2 = find_or_create(&pool, user.id, "CNY").await.unwrap(); + let w1 = find_or_create(&pool, *user.id, "CNY").await.unwrap(); + let w2 = find_or_create(&pool, *user.id, "CNY").await.unwrap(); assert_eq!(w1.id, w2.id); } @@ -208,8 +207,8 @@ mod tests { let pool = setup_pool().await; let user1 = insert_user(&pool).await; let user2 = insert_user(&pool).await; - create(&pool, user1.id, "CNY").await.unwrap(); - create(&pool, user2.id, "CNY").await.unwrap(); + create(&pool, *user1.id, "CNY").await.unwrap(); + create(&pool, *user2.id, "CNY").await.unwrap(); let (rows, total) = find_all_wallets(&pool, 1, 10, None).await.unwrap(); assert_eq!(total, 2); assert_eq!(rows.len(), 2); @@ -219,7 +218,7 @@ mod tests { async fn find_all_wallets_page_two_empty() { let pool = setup_pool().await; let user = insert_user(&pool).await; - create(&pool, user.id, "CNY").await.unwrap(); + create(&pool, *user.id, "CNY").await.unwrap(); let (rows, total) = find_all_wallets(&pool, 2, 10, None).await.unwrap(); assert_eq!(total, 1); assert!(rows.is_empty()); diff --git a/src/models/wallet_outbox.rs b/src/models/wallet_outbox.rs index e57d6ebf..04f1b2a0 100644 --- a/src/models/wallet_outbox.rs +++ b/src/models/wallet_outbox.rs @@ -3,6 +3,7 @@ use sqlx::FromRow; use crate::db::dialect::ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -17,9 +18,8 @@ define_enum!( #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct WalletOutbox { - pub id: i64, - pub document_id: String, - pub user_id: i64, + pub id: SnowflakeId, + pub user_id: SnowflakeId, pub currency: String, pub amount: i64, pub entry_type: String, @@ -42,13 +42,13 @@ pub async fn tx_insert( cmd: &crate::commands::CreateWalletOutboxCmd, tenant_id: Option<&str>, ) -> AppResult<()> { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( &mut *tx, "wallet_outbox", [ - "document_id" => &document_id, + "id" => id, "user_id" => cmd.user_id, "currency" => &cmd.currency, "amount" => cmd.amount, diff --git a/src/models/wallet_transaction.rs b/src/models/wallet_transaction.rs index 999c1eb3..15759f0a 100644 --- a/src/models/wallet_transaction.rs +++ b/src/models/wallet_transaction.rs @@ -1,8 +1,8 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use crate::db::dialect::ph; use crate::errors::app_error::AppResult; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -38,20 +38,19 @@ define_enum!( #[derive(Debug, FromRow, Serialize, Deserialize, Clone)] pub struct WalletTransaction { - pub id: i64, - pub document_id: String, - pub wallet_id: i64, - pub user_id: i64, + pub id: SnowflakeId, + pub wallet_id: SnowflakeId, + pub user_id: SnowflakeId, pub entry_type: WalletEntryType, pub amount: i64, pub balance_after: i64, pub tx_type: WalletTxType, pub currency: String, pub transaction_no: String, - pub related_tx_id: Option, + pub related_tx_id: Option, pub reference_type: Option, pub reference_id: Option, - pub counterparty_wallet_id: Option, + pub counterparty_wallet_id: Option, pub metadata: Option, pub created_at: Timestamp, } @@ -127,14 +126,6 @@ pub async fn find_tx_by_id( .map_err(Into::into) } -pub async fn find_tx_by_document_id( - pool: &crate::db::Pool, - document_id: &str, -) -> AppResult> { - raisfast_derive::crud_find!(pool, "wallet_transactions", WalletTransaction, "document_id" => document_id) - .map_err(Into::into) -} - pub async fn has_reversal_for(pool: &crate::db::Pool, related_tx_id: i64) -> AppResult { let count: i64 = raisfast_derive::crud_count!( pool, "wallet_transactions", "related_tx_id" => related_tx_id, @@ -143,27 +134,6 @@ pub async fn has_reversal_for(pool: &crate::db::Pool, related_tx_id: i64) -> App Ok(count > 0) } -pub async fn find_document_ids_by_ids( - pool: &crate::db::Pool, - ids: &[i64], -) -> AppResult> { - raisfast_derive::check_schema!("wallet_transactions", "id", "document_id"); - if ids.is_empty() { - return Ok(std::collections::HashMap::new()); - } - let placeholders: Vec = ids.iter().enumerate().map(|(i, _)| ph(i + 1)).collect(); - let sql = format!( - "SELECT id, document_id FROM wallet_transactions WHERE id IN ({})", - placeholders.join(", ") - ); - let mut query = sqlx::query_as::<_, (i64, String)>(&sql); - for &id in ids { - query = query.bind(id); - } - let rows = query.fetch_all(pool).await?; - Ok(rows.into_iter().collect()) -} - #[cfg(test)] mod tests { use super::*; @@ -176,7 +146,7 @@ mod tests { crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, @@ -193,20 +163,20 @@ mod tests { WalletTransaction, ) { let user = insert_user(pool).await; - let w = crate::models::wallet::create(pool, user.id, "CNY") + let w = crate::models::wallet::create(pool, *user.id, "CNY") .await .unwrap(); - let (doc_id, now) = crate::utils::id::new_document_id_and_timestamp(); - let tx_no = format!("TX_{doc_id}"); + let (tx_id, now) = crate::utils::id::new_id_and_timestamp(); + let tx_no = format!("TX_{tx_id}"); sqlx::query(&format!( - "INSERT INTO wallet_transactions (document_id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {})", + "INSERT INTO wallet_transactions (id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {})", crate::db::dialect::ph(1), crate::db::dialect::ph(2), crate::db::dialect::ph(3), crate::db::dialect::ph(4), crate::db::dialect::ph(5), crate::db::dialect::ph(6), crate::db::dialect::ph(7), crate::db::dialect::ph(8), crate::db::dialect::ph(9), crate::db::dialect::ph(10), )) - .bind(&doc_id) + .bind(tx_id) .bind(w.id) .bind(user.id) .bind(WalletEntryType::Credit) @@ -254,7 +224,7 @@ mod tests { async fn find_tx_by_id_found() { let pool = setup_pool().await; let (_, _, tx) = seed_wallet_and_tx(&pool).await; - let found = find_tx_by_id(&pool, tx.id).await.unwrap().unwrap(); + let found = find_tx_by_id(&pool, *tx.id).await.unwrap().unwrap(); assert_eq!(found.transaction_no, tx.transaction_no); } @@ -268,7 +238,7 @@ mod tests { async fn find_transactions_by_wallet_found() { let pool = setup_pool().await; let (_, w, _) = seed_wallet_and_tx(&pool).await; - let (rows, total) = find_transactions_by_wallet(&pool, w.id, 1, 10) + let (rows, total) = find_transactions_by_wallet(&pool, *w.id, 1, 10) .await .unwrap(); assert_eq!(total, 1); @@ -290,7 +260,7 @@ mod tests { async fn find_transactions_by_user_found() { let pool = setup_pool().await; let (user, _, _) = seed_wallet_and_tx(&pool).await; - let (rows, total) = find_transactions_by_user(&pool, user.id, 1, 10) + let (rows, total) = find_transactions_by_user(&pool, *user.id, 1, 10) .await .unwrap(); assert_eq!(total, 1); @@ -301,7 +271,7 @@ mod tests { async fn has_reversal_for_false() { let pool = setup_pool().await; let (_, _, tx) = seed_wallet_and_tx(&pool).await; - assert!(!has_reversal_for(&pool, tx.id).await.unwrap()); + assert!(!has_reversal_for(&pool, *tx.id).await.unwrap()); } #[tokio::test] @@ -309,16 +279,16 @@ mod tests { let pool = setup_pool().await; let (_, _, tx) = seed_wallet_and_tx(&pool).await; - let (rev_doc_id, rev_now) = crate::utils::id::new_document_id_and_timestamp(); - let rev_no = format!("REV_{rev_doc_id}"); + let (rev_id, rev_now) = crate::utils::id::new_id_and_timestamp(); + let rev_no = format!("REV_{rev_id}"); sqlx::query(&format!( - "INSERT INTO wallet_transactions (document_id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, related_tx_id, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})", + "INSERT INTO wallet_transactions (id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, related_tx_id, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})", crate::db::dialect::ph(1), crate::db::dialect::ph(2), crate::db::dialect::ph(3), crate::db::dialect::ph(4), crate::db::dialect::ph(5), crate::db::dialect::ph(6), crate::db::dialect::ph(7), crate::db::dialect::ph(8), crate::db::dialect::ph(9), crate::db::dialect::ph(10), crate::db::dialect::ph(11), )) - .bind(&rev_doc_id) + .bind(rev_id) .bind(tx.wallet_id) .bind(tx.user_id) .bind(WalletEntryType::Debit) @@ -333,6 +303,6 @@ mod tests { .await .unwrap(); - assert!(has_reversal_for(&pool, tx.id).await.unwrap()); + assert!(has_reversal_for(&pool, *tx.id).await.unwrap()); } } diff --git a/src/payment/providers/alipay.rs b/src/payment/providers/alipay.rs index 95567d02..bf732b2f 100644 --- a/src/payment/providers/alipay.rs +++ b/src/payment/providers/alipay.rs @@ -293,7 +293,7 @@ impl PaymentProvider for AlipayProvider { let private_key = parse_private_key(&creds.private_key)?; let biz = PrecreateBizContent { - out_trade_no: order.document_id.clone(), + out_trade_no: order.id.to_string(), total_amount: cents_to_amount(order.amount), subject: order.title.clone(), timeout_express: Some("30m".to_string()), @@ -323,7 +323,7 @@ impl PaymentProvider for AlipayProvider { provider_order_id: result .trade_no .or(result.out_trade_no) - .unwrap_or(order.document_id.clone()), + .unwrap_or(order.id.to_string()), redirect_url: None, qr_code: result.qr_code, client_secret: None, @@ -650,7 +650,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "alipay".into(), name: "Alipay".into(), diff --git a/src/payment/providers/creem.rs b/src/payment/providers/creem.rs index ca909fbd..720aa8f8 100644 --- a/src/payment/providers/creem.rs +++ b/src/payment/providers/creem.rs @@ -210,7 +210,7 @@ impl PaymentProvider for CreemProvider { let mut metadata = serde_json::Map::new(); metadata.insert( "payment_order_id".to_string(), - serde_json::Value::String(order.document_id.clone()), + serde_json::Value::String(order.id.to_string()), ); if let Some(ref order_id) = order.order_id { metadata.insert( @@ -440,7 +440,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -463,7 +462,6 @@ mod tests { fn extract_product_id_missing_settings() { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -492,7 +490,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -541,7 +538,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -594,7 +590,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -625,7 +620,6 @@ mod tests { let key = test_key(); let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), @@ -649,7 +643,6 @@ mod tests { let key = test_key(); let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "creem".into(), name: "Creem".into(), diff --git a/src/payment/providers/dodo.rs b/src/payment/providers/dodo.rs index 1246e3b0..8d54c2f3 100644 --- a/src/payment/providers/dodo.rs +++ b/src/payment/providers/dodo.rs @@ -337,7 +337,7 @@ impl PaymentProvider for DodoProvider { let mut metadata = serde_json::Map::new(); metadata.insert( "payment_order_id".to_string(), - serde_json::Value::String(order.document_id.clone()), + serde_json::Value::String(order.id.to_string()), ); if let Some(ref order_id) = order.order_id { metadata.insert( @@ -533,7 +533,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), @@ -556,7 +555,6 @@ mod tests { fn extract_product_id_missing_settings() { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), @@ -585,7 +583,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), @@ -697,7 +694,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), @@ -763,7 +759,6 @@ mod tests { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), @@ -793,7 +788,6 @@ mod tests { let key = test_key(); let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "dodo".into(), name: "Dodo".into(), diff --git a/src/payment/providers/stripe.rs b/src/payment/providers/stripe.rs index 466e07fd..23569791 100644 --- a/src/payment/providers/stripe.rs +++ b/src/payment/providers/stripe.rs @@ -78,7 +78,7 @@ impl PaymentProvider for StripeProvider { let mut params = stripe::CreatePaymentIntent::new(order.amount, currency); let mut meta = HashMap::new(); - meta.insert("payment_order_id".to_string(), order.document_id.clone()); + meta.insert("payment_order_id".to_string(), order.id.to_string()); params.metadata = Some(stripe::Metadata::from(meta)); if let Some(url) = return_url { diff --git a/src/payment/providers/wechat.rs b/src/payment/providers/wechat.rs index 19515753..1d0e9ae6 100644 --- a/src/payment/providers/wechat.rs +++ b/src/payment/providers/wechat.rs @@ -343,7 +343,7 @@ impl PaymentProvider for WechatPayProvider { appid: creds.appid.clone(), mchid: creds.mchid.clone(), description: order.title.clone(), - out_trade_no: order.document_id.clone(), + out_trade_no: order.id.to_string(), time_expire: Some(expire.to_rfc3339()), notify_url: extract_notify_url(channel), amount: NativeAmount { @@ -365,7 +365,7 @@ impl PaymentProvider for WechatPayProvider { .await?; Ok(ProviderResponse { - provider_order_id: order.document_id.clone(), + provider_order_id: order.id.to_string(), redirect_url: None, qr_code: resp.code_url, client_secret: None, @@ -616,7 +616,6 @@ mod tests { fn extract_notify_url_works() { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "wechat".into(), name: "Wechat".into(), @@ -640,7 +639,6 @@ mod tests { fn extract_notify_url_missing() { let channel = PaymentChannel { id: 1, - document_id: "doc1".into(), tenant_id: None, provider: "wechat".into(), name: "Wechat".into(), diff --git a/src/payment/routing.rs b/src/payment/routing.rs index d52d08db..cd51d871 100644 --- a/src/payment/routing.rs +++ b/src/payment/routing.rs @@ -142,8 +142,7 @@ mod tests { fn make_channel(provider: &str, settings: Option<&str>) -> PaymentChannel { PaymentChannel { - id: 1, - document_id: format!("doc_{provider}"), + id: crate::utils::id::SnowflakeId(1), tenant_id: None, provider: provider.to_string(), name: provider.to_string(), diff --git a/src/plugins.rs b/src/plugins.rs index 716de5c3..5ae6dae9 100644 --- a/src/plugins.rs +++ b/src/plugins.rs @@ -2022,7 +2022,7 @@ mod tests { let config = test_config(); let mgr = PluginManager::new(config).await; let auth = crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ); @@ -2230,7 +2230,7 @@ export function handle_test(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -2954,7 +2954,7 @@ end None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -2969,7 +2969,7 @@ end None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -2984,7 +2984,7 @@ end None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -3306,7 +3306,7 @@ fn handle_test(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -3618,7 +3618,7 @@ fn seo_health(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -3634,7 +3634,7 @@ fn seo_health(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -3650,7 +3650,7 @@ fn seo_health(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), @@ -3666,7 +3666,7 @@ fn seo_health(input) { None, None, &crate::middleware::auth::AuthUser::new_test( - "", + 0, crate::models::user::UserRole::Reader, "", ), diff --git a/src/plugins/host_common.rs b/src/plugins/host_common.rs index 38dd20be..21d25a4f 100644 --- a/src/plugins/host_common.rs +++ b/src/plugins/host_common.rs @@ -368,7 +368,7 @@ impl HostContext { /// `params_json` is a JSON array string, corresponding in order to the `host.ph(N)` placeholders in the SQL. /// Example: /// ```js - /// const sql = `INSERT INTO tags (document_id, name) VALUES (${host.ph(1)}, ${host.ph(2)})`; + /// const sql = `INSERT INTO tags (id, name) VALUES (${host.ph(1)}, ${host.ph(2)})`; /// host.dbExecute(sql, JSON.stringify(["tag-1", "Rust"])); /// ``` #[must_use] @@ -1834,19 +1834,16 @@ mod tests { let ctx = HostContext::new("test", config, "p1".into(), perms, Some(pool)); let result = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES ('tag-1', 'Test', 'test')", + "INSERT INTO tags (name, slug) VALUES ('Test', 'test')", "[]", ); assert!(result.contains("rows_affected")); assert!(!result.contains("error")); - let update = ctx.db_execute( - "UPDATE tags SET name = 'Updated' WHERE document_id = 'tag-1'", - "[]", - ); + let update = ctx.db_execute("UPDATE tags SET name = 'Updated' WHERE slug = 'test'", "[]"); assert!(update.contains("rows_affected")); - let delete = ctx.db_execute("DELETE FROM tags WHERE document_id = 'tag-1'", "[]"); + let delete = ctx.db_execute("DELETE FROM tags WHERE slug = 'test'", "[]"); assert!(delete.contains("rows_affected")); } @@ -1866,19 +1863,19 @@ mod tests { let ctx = HostContext::new("test", config, "p1".into(), perms, Some(pool)); let result = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES (?, ?, ?)", - r#"["t2","Param Tag","param-tag"]"#, + "INSERT INTO tags (name, slug) VALUES (?, ?)", + r#"["Param Tag","param-tag"]"#, ); assert!(result.contains("rows_affected")); assert!(!result.contains("error")); let update = ctx.db_execute( - "UPDATE tags SET name = ? WHERE document_id = ?", - r#"["Renamed","t2"]"#, + "UPDATE tags SET name = ? WHERE slug = ?", + r#"["Renamed","param-tag"]"#, ); assert!(update.contains("rows_affected")); - let delete = ctx.db_execute("DELETE FROM tags WHERE document_id = ?", r#"["t2"]"#); + let delete = ctx.db_execute("DELETE FROM tags WHERE slug = ?", r#"["param-tag"]"#); assert!(delete.contains("rows_affected")); } @@ -1890,10 +1887,7 @@ mod tests { ..Permissions::default() }; let ctx = HostContext::new("test", config, "p1".into(), perms, None); - let result = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES (?)", - "not valid json", - ); + let result = ctx.db_execute("INSERT INTO tags (name, slug) VALUES (?)", "not valid json"); assert!(result.contains("invalid params JSON")); } @@ -1906,7 +1900,7 @@ mod tests { }; let ctx = HostContext::new("test", config, "p1".into(), perms, None); let result = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES (?, ?, ?)", + "INSERT INTO tags (name, slug) VALUES (?, ?)", r#"[{"nested":"object"}]"#, ); assert!(result.contains("unsupported param type")); @@ -1955,7 +1949,7 @@ mod tests { assert!(begin.contains(r#""ok":true"#), "begin failed: {begin}"); let insert = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES ('tx-1', 'TxTest', 'tx-test')", + "INSERT INTO tags (name, slug) VALUES ('TxTest', 'tx-test')", "[]", ); assert!(insert.contains("rows_affected"), "insert failed: {insert}"); @@ -1963,12 +1957,10 @@ mod tests { let commit = ctx.db_commit(); assert!(commit.contains(r#""ok":true"#), "commit failed: {commit}"); - let rows: Vec<(String,)> = - sqlx::query_as("SELECT document_id FROM tags WHERE document_id = 'tx-1'") - .bind("tx-1") - .fetch_all(&pool) - .await - .unwrap(); + let rows: Vec<(String,)> = sqlx::query_as("SELECT name FROM tags WHERE slug = 'tx-test'") + .fetch_all(&pool) + .await + .unwrap(); assert_eq!(rows.len(), 1, "row should be committed"); } @@ -1991,7 +1983,7 @@ mod tests { assert!(begin.contains(r#""ok":true"#)); let insert = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES ('rb-1', 'RbTest', 'rb-test')", + "INSERT INTO tags (name, slug) VALUES ('RbTest', 'rb-test')", "[]", ); assert!(insert.contains("rows_affected")); @@ -1999,7 +1991,7 @@ mod tests { let rollback = ctx.db_rollback(); assert!(rollback.contains(r#""ok":true"#)); - let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tags WHERE document_id = 'rb-1'") + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tags WHERE slug = 'rb-test'") .fetch_one(&pool) .await .unwrap(); @@ -2047,14 +2039,14 @@ mod tests { assert!(begin.contains(r#""ok":true"#)); let insert = ctx.db_execute( - "INSERT INTO tags (document_id, name, slug) VALUES ('cl-1', 'CleanTest', 'cl-test')", + "INSERT INTO tags (name, slug) VALUES ('CleanTest', 'cl-test')", "[]", ); assert!(insert.contains("rows_affected")); ctx.cleanup_tx(); - let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tags WHERE document_id = 'cl-1'") + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tags WHERE slug = 'cl-test'") .fetch_one(&pool) .await .unwrap(); @@ -2101,14 +2093,10 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let result = ctx.db_insert( - "tags", - r#"{"document_id":"t1","name":"Rust","slug":"rust"}"#, - "{}", - ); + let result = ctx.db_insert("tags", r#"{"name":"Rust","slug":"rust"}"#, "{}"); assert!(result.contains(r#""rows_affected":1"#), "insert: {result}"); - let found = ctx.db_fetch_one("tags", r#"{"document_id":"t1"}"#, "{}"); + let found = ctx.db_fetch_one("tags", r#"{"slug":"rust"}"#, "{}"); assert!(found.contains(r#""name":"Rust""#), "fetch_one: {found}"); } @@ -2121,7 +2109,7 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let found = ctx.db_fetch_one("tags", r#"{"document_id":"nonexistent"}"#, "{}"); + let found = ctx.db_fetch_one("tags", r#"{"slug":"nonexistent"}"#, "{}"); assert!(found.contains(r#""data":null"#), "not found: {found}"); } @@ -2134,11 +2122,7 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"t2","name":"Go","slug":"go"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Go","slug":"go"}"#, "{}"); let found = ctx.db_fetch_one("tags", r#"name = 'Go'"#, "{}"); assert!(found.contains(r#""slug":"go""#), "string where: {found}"); @@ -2153,17 +2137,10 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"t3","name":"Python","slug":"python"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Python","slug":"python"}"#, "{}"); let found = ctx.db_fetch_one("tags", r#"["name = ?", "Python"]"#, "{}"); - assert!( - found.contains(r#""document_id":"t3""#), - "array where: {found}" - ); + assert!(found.contains(r#""slug":"python""#), "array where: {found}"); } #[tokio::test(flavor = "multi_thread")] @@ -2175,21 +2152,9 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"a1","name":"A","slug":"a"}"#, - "{}", - ); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"b2","name":"B","slug":"b"}"#, - "{}", - ); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"c3","name":"C","slug":"c"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"A","slug":"a"}"#, "{}"); + let _ = ctx.db_insert("tags", r#"{"name":"B","slug":"b"}"#, "{}"); + let _ = ctx.db_insert("tags", r#"{"name":"C","slug":"c"}"#, "{}"); let result = ctx.db_fetch_all("tags", "{}", r#"{"order_by":"name DESC","limit":2}"#); assert!(result.contains(r#""total":2"#), "fetch_all limit: {result}"); @@ -2204,16 +2169,12 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"u1","name":"Old","slug":"old"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Old","slug":"old"}"#, "{}"); - let result = ctx.db_update("tags", r#"{"name":"New"}"#, r#"{"document_id":"u1"}"#, "{}"); + let result = ctx.db_update("tags", r#"{"name":"New"}"#, r#"{"slug":"old"}"#, "{}"); assert!(result.contains(r#""rows_affected":1"#), "update: {result}"); - let found = ctx.db_fetch_one("tags", r#"{"document_id":"u1"}"#, "{}"); + let found = ctx.db_fetch_one("tags", r#"{"slug":"old"}"#, "{}"); assert!(found.contains(r#""name":"New""#), "after update: {found}"); } @@ -2239,16 +2200,12 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"d1","name":"Delete","slug":"del"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Delete","slug":"del"}"#, "{}"); - let result = ctx.db_delete("tags", r#"{"document_id":"d1"}"#, "{}"); + let result = ctx.db_delete("tags", r#"{"slug":"del"}"#, "{}"); assert!(result.contains(r#""rows_affected":1"#), "delete: {result}"); - let found = ctx.db_fetch_one("tags", r#"{"document_id":"d1"}"#, "{}"); + let found = ctx.db_fetch_one("tags", r#"{"slug":"del"}"#, "{}"); assert!(found.contains(r#""data":null"#), "after delete: {found}"); } @@ -2261,16 +2218,8 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"c1","name":"Count1","slug":"c1"}"#, - "{}", - ); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"c2","name":"Count2","slug":"c2"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Count1","slug":"c1"}"#, "{}"); + let _ = ctx.db_insert("tags", r#"{"name":"Count2","slug":"c2"}"#, "{}"); let result = ctx.db_count("tags", "{}", "{}"); assert!(result.contains(r#""count":2"#), "count: {result}"); @@ -2285,16 +2234,8 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"cw1","name":"Go","slug":"go"}"#, - "{}", - ); - let _ = ctx.db_insert( - "tags", - r#"{"document_id":"cw2","name":"Rust","slug":"rust"}"#, - "{}", - ); + let _ = ctx.db_insert("tags", r#"{"name":"Go","slug":"go"}"#, "{}"); + let _ = ctx.db_insert("tags", r#"{"name":"Rust","slug":"rust"}"#, "{}"); let result = ctx.db_count("tags", r#"["name = ?", "Rust"]"#, "{}"); assert!( @@ -2378,30 +2319,30 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"p1","name":"Test","slug":"test","sort_order":"0"}"#, + r#"{"name":"Test","slug":"test","sort_order":"0"}"#, "{}", ); let r = ctx.db_increment( "categories", r#"{"sort_order":1}"#, - r#"{"document_id":"p1"}"#, + r#"{"slug":"test"}"#, "{}", ); assert!(r.contains(r#""rows_affected":1"#), "increment: {r}"); - let s = ctx.db_sum("categories", "sort_order", r#"{"document_id":"p1"}"#, "{}"); + let s = ctx.db_sum("categories", "sort_order", r#"{"slug":"test"}"#, "{}"); assert!(s.contains(r#""sum":1"#), "after increment sum: {s}"); let r2 = ctx.db_increment( "categories", r#"{"sort_order":1}"#, - r#"{"document_id":"p1"}"#, + r#"{"slug":"test"}"#, "{}", ); assert!(r2.contains(r#""rows_affected":1"#)); - let s2 = ctx.db_sum("categories", "sort_order", r#"{"document_id":"p1"}"#, "{}"); + let s2 = ctx.db_sum("categories", "sort_order", r#"{"slug":"test"}"#, "{}"); assert!(s2.contains(r#""sum":2"#), "after 2nd sum: {s2}"); } @@ -2416,19 +2357,19 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"p2","name":"Dec","slug":"dec","sort_order":"5"}"#, + r#"{"name":"Dec","slug":"dec","sort_order":"5"}"#, "{}", ); let r = ctx.db_increment( "categories", r#"{"sort_order":-1}"#, - r#"{"document_id":"p2"}"#, + r#"{"slug":"dec"}"#, "{}", ); assert!(r.contains(r#""rows_affected":1"#), "decrement: {r}"); - let s = ctx.db_sum("categories", "sort_order", r#"{"document_id":"p2"}"#, "{}"); + let s = ctx.db_sum("categories", "sort_order", r#"{"slug":"dec"}"#, "{}"); assert!(s.contains(r#""sum":4"#), "after decrement sum: {s}"); } @@ -2443,19 +2384,19 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"p3","name":"Clamp","slug":"clamp","sort_order":"1"}"#, + r#"{"name":"Clamp","slug":"clamp","sort_order":"1"}"#, "{}", ); let r = ctx.db_increment( "categories", r#"{"sort_order":-5}"#, - r#"{"document_id":"p3"}"#, + r#"{"slug":"clamp"}"#, r#"{"min":0}"#, ); assert!(r.contains(r#""rows_affected":1"#), "clamp: {r}"); - let s = ctx.db_sum("categories", "sort_order", r#"{"document_id":"p3"}"#, "{}"); + let s = ctx.db_sum("categories", "sort_order", r#"{"slug":"clamp"}"#, "{}"); assert!(s.contains(r#""sum":0"#), "clamped to 0 sum: {s}"); } @@ -2470,22 +2411,22 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"p4","name":"Set","slug":"set","sort_order":"0"}"#, + r#"{"name":"Set","slug":"set","sort_order":"0"}"#, "{}", ); let r = ctx.db_increment( "categories", r#"{"sort_order":1}"#, - r#"{"document_id":"p4"}"#, + r#"{"slug":"set"}"#, r#"{"set":{"name":"Updated"}}"#, ); assert!(r.contains(r#""rows_affected":1"#), "increment+set: {r}"); - let s = ctx.db_sum("categories", "sort_order", r#"{"document_id":"p4"}"#, "{}"); + let s = ctx.db_sum("categories", "sort_order", r#"{"slug":"set"}"#, "{}"); assert!(s.contains(r#""sum":1"#), "incremented sum: {s}"); - let found = ctx.db_fetch_one("categories", r#"{"document_id":"p4"}"#, "{}"); + let found = ctx.db_fetch_one("categories", r#"{"slug":"set"}"#, "{}"); assert!(found.contains(r#""name":"Updated""#), "set col: {found}"); } @@ -2531,12 +2472,12 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"s1","name":"A","slug":"sa","sort_order":"3"}"#, + r#"{"name":"A","slug":"sa","sort_order":"3"}"#, "{}", ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"s2","name":"B","slug":"sb","sort_order":"7"}"#, + r#"{"name":"B","slug":"sb","sort_order":"7"}"#, "{}", ); @@ -2585,17 +2526,17 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"g1","name":"Rust1","slug":"rust1","tenant_id":"t1"}"#, + r#"{"name":"Rust1","slug":"rust1","tenant_id":"t1"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"g2","name":"Go","slug":"go","tenant_id":"t2"}"#, + r#"{"name":"Go","slug":"go","tenant_id":"t2"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"g3","name":"Rust2","slug":"rust2","tenant_id":"t1"}"#, + r#"{"name":"Rust2","slug":"rust2","tenant_id":"t1"}"#, r#"{"tenant":"disabled"}"#, ); @@ -2619,17 +2560,17 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"gs1","name":"A1","slug":"gsa","sort_order":"3","tenant_id":"grpA"}"#, + r#"{"name":"A1","slug":"gsa","sort_order":"3","tenant_id":"grpA"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"gs2","name":"A2","slug":"gsb","sort_order":"7","tenant_id":"grpA"}"#, + r#"{"name":"A2","slug":"gsb","sort_order":"7","tenant_id":"grpA"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"gs3","name":"B1","slug":"gsc","sort_order":"2","tenant_id":"grpB"}"#, + r#"{"name":"B1","slug":"gsc","sort_order":"2","tenant_id":"grpB"}"#, r#"{"tenant":"disabled"}"#, ); @@ -2655,17 +2596,17 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"w1","name":"W1","slug":"wa","tenant_id":"tA"}"#, + r#"{"name":"W1","slug":"wa","tenant_id":"tA"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"w2","name":"W2","slug":"wb","tenant_id":"tA"}"#, + r#"{"name":"W2","slug":"wb","tenant_id":"tA"}"#, r#"{"tenant":"disabled"}"#, ); let _ = ctx.db_insert( "categories", - r#"{"document_id":"w3","name":"W3","slug":"wc","tenant_id":"tB"}"#, + r#"{"name":"W3","slug":"wc","tenant_id":"tB"}"#, r#"{"tenant":"disabled"}"#, ); @@ -2686,21 +2627,9 @@ mod tests { .unwrap(); let ctx = make_crud_ctx(&pool); - let _ = ctx.db_insert( - "categories", - r#"{"document_id":"l1","name":"X","slug":"lx"}"#, - "{}", - ); - let _ = ctx.db_insert( - "categories", - r#"{"document_id":"l2","name":"Y","slug":"ly"}"#, - "{}", - ); - let _ = ctx.db_insert( - "categories", - r#"{"document_id":"l3","name":"Z","slug":"lz"}"#, - "{}", - ); + let _ = ctx.db_insert("categories", r#"{"name":"X","slug":"lx"}"#, "{}"); + let _ = ctx.db_insert("categories", r#"{"name":"Y","slug":"ly"}"#, "{}"); + let _ = ctx.db_insert("categories", r#"{"name":"Z","slug":"lz"}"#, "{}"); let r = ctx.db_group_by( "categories", @@ -2764,19 +2693,19 @@ mod tests { let _ = ctx.db_insert( "categories", - r#"{"document_id":"m1","name":"Multi","slug":"multi","sort_order":"2"}"#, + r#"{"name":"Multi","slug":"multi","sort_order":"2"}"#, "{}", ); let r = ctx.db_increment( "categories", r#"{"sort_order":-10}"#, - r#"{"document_id":"m1"}"#, + r#"{"slug":"multi"}"#, r#"{"min":0}"#, ); assert!(r.contains(r#""rows_affected":1"#), "multi clamp: {r}"); - let s = ctx.db_sum("categories", "sort_order", r#"{"document_id":"m1"}"#, "{}"); + let s = ctx.db_sum("categories", "sort_order", r#"{"slug":"multi"}"#, "{}"); assert!(s.contains(r#""sum":0"#), "clamped sum: {s}"); } } diff --git a/src/policy.rs b/src/policy.rs index 974d916d..f59e4f82 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -24,7 +24,7 @@ pub trait Policy { } fn owner_or_admin(user: &AuthUser, owner_id: i64) -> AppResult<()> { - let uid = user.user_int_id().ok_or(AppError::Unauthorized)?; + let uid = user.user_id().ok_or(AppError::Unauthorized)?; if user.is_admin() || uid == owner_id { Ok(()) } else { @@ -32,9 +32,12 @@ fn owner_or_admin(user: &AuthUser, owner_id: i64) -> AppResult<()> { } } -fn owner_or_admin_opt(user: &AuthUser, owner_id: Option) -> AppResult<()> { - let uid = user.user_int_id().ok_or(AppError::Unauthorized)?; - if user.is_admin() || owner_id == Some(uid) { +fn owner_or_admin_opt( + user: &AuthUser, + owner_id: Option, +) -> AppResult<()> { + let uid = user.user_id().ok_or(AppError::Unauthorized)?; + if user.is_admin() || owner_id == Some(crate::utils::id::SnowflakeId(uid)) { Ok(()) } else { Err(AppError::Forbidden) @@ -48,11 +51,11 @@ impl Policy for PostPolicy { type Resource = crate::models::post::Post; fn can_update(user: &AuthUser, post: &Self::Resource) -> AppResult<()> { - owner_or_admin(user, post.created_by) + owner_or_admin(user, *post.created_by) } fn can_delete(user: &AuthUser, post: &Self::Resource) -> AppResult<()> { - owner_or_admin(user, post.created_by) + owner_or_admin(user, *post.created_by) } } @@ -77,27 +80,22 @@ mod tests { use crate::models::user::UserRole; fn admin() -> AuthUser { - AuthUser::from_parts(Some("u".into()), Some(1), UserRole::Admin, Some("t".into())) + AuthUser::from_parts(Some(1), UserRole::Admin, Some("t".into())) } fn author(uid: i64) -> AuthUser { - AuthUser::from_parts( - Some("u".into()), - Some(uid), - UserRole::Author, - Some("t".into()), - ) + AuthUser::from_parts(Some(uid), UserRole::Author, Some("t".into())) } fn anon() -> AuthUser { - AuthUser::from_parts(None, None, UserRole::Reader, Some("t".into())) + AuthUser::from_parts(None, UserRole::Reader, Some("t".into())) } fn mock_post(created_by: i64) -> crate::models::post::Post { + use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; crate::models::post::Post { - id: 1, - document_id: "doc".into(), + id: SnowflakeId(1), tenant_id: None, title: String::new(), slug: String::new(), @@ -105,7 +103,7 @@ mod tests { excerpt: None, cover_image: None, status: crate::models::post::PostStatus::Draft, - created_by, + created_by: SnowflakeId(created_by), updated_by: None, category_id: None, view_count: 0, @@ -128,13 +126,13 @@ mod tests { } fn mock_comment(created_by: Option) -> crate::models::comment::Comment { + use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; crate::models::comment::Comment { - id: 1, - document_id: "doc".into(), + id: SnowflakeId(1), tenant_id: None, - post_id: 1, - created_by, + post_id: SnowflakeId(1), + created_by: created_by.map(SnowflakeId), updated_by: None, nickname: None, email: None, diff --git a/src/protocols.rs b/src/protocols.rs index 151657ac..085cf94f 100644 --- a/src/protocols.rs +++ b/src/protocols.rs @@ -230,7 +230,7 @@ pub trait Protocol: Send + Sync + 'static { &self, _pool: &crate::db::pool::Pool, _content_type_singular: &str, - _record_id: &str, + _record_id: i64, ) -> std::pin::Pin> + Send + '_>> { Box::pin(async { Ok(()) }) @@ -374,7 +374,7 @@ impl ProtocolRegistry { names: &[String], pool: &crate::db::pool::Pool, content_type_singular: &str, - record_id: &str, + record_id: i64, ) -> Result<(), anyhow::Error> { for name in names { if let Some(protocol) = self.protocols.get(name.as_str()) { diff --git a/src/protocols/versionable.rs b/src/protocols/versionable.rs index 62851796..489dbc3f 100644 --- a/src/protocols/versionable.rs +++ b/src/protocols/versionable.rs @@ -79,14 +79,14 @@ impl Protocol for VersionableProtocol { &self, pool: &crate::db::pool::Pool, content_type_singular: &str, - record_id: &str, + record_id: i64, ) -> std::pin::Pin> + Send + '_>> { let pool = pool.clone(); let singular = content_type_singular.to_string(); - let id = record_id.to_string(); + let id = record_id; Box::pin(async move { - let _ = crate::models::content_revision::delete_revisions(&pool, &singular, &id).await; + let _ = crate::models::content_revision::delete_revisions(&pool, &singular, id).await; Ok(()) }) } diff --git a/src/server.rs b/src/server.rs index df93ebea..60d19e22 100644 --- a/src/server.rs +++ b/src/server.rs @@ -224,9 +224,9 @@ async fn build_app( for (method, suffix) in [ ("GET", ""), ("POST", ""), - ("GET", "/{id_or_slug}"), - ("PUT", "/{id_or_slug}"), - ("DELETE", "/{id_or_slug}"), + ("GET", "/{id}"), + ("PUT", "/{id}"), + ("DELETE", "/{id}"), ] { registry.record( method, @@ -243,11 +243,7 @@ async fn build_app( ); registry.record( "GET", - &format!( - "{}/{}/{{id_or_slug}}", - crate::constants::CMS_ADMIN_PREFIX, - plural - ), + &format!("{}/{}/{{id}}", crate::constants::CMS_ADMIN_PREFIX, plural), "content_type", name, ); @@ -266,27 +262,19 @@ async fn build_app( ); registry.record( "GET", - &format!("{}/{}/{{id_or_slug}}", crate::constants::CMS_PREFIX, plural), + &format!("{}/{}/{{id}}", crate::constants::CMS_PREFIX, plural), "content_type", name, ); registry.record( "POST", - &format!( - "{}/{}/{{id_or_slug}}/update", - crate::constants::CMS_PREFIX, - plural - ), + &format!("{}/{}/{{id}}/update", crate::constants::CMS_PREFIX, plural), "content_type", name, ); registry.record( "POST", - &format!( - "{}/{}/{{id_or_slug}}/delete", - crate::constants::CMS_PREFIX, - plural - ), + &format!("{}/{}/{{id}}/delete", crate::constants::CMS_PREFIX, plural), "content_type", name, ); @@ -298,11 +286,7 @@ async fn build_app( ); registry.record( "GET", - &format!( - "{}/{}/{{id_or_slug}}", - crate::constants::CMS_ADMIN_PREFIX, - plural - ), + &format!("{}/{}/{{id}}", crate::constants::CMS_ADMIN_PREFIX, plural), "content_type", name, ); diff --git a/src/services/api_token.rs b/src/services/api_token.rs index d1dbe44e..67ac98f9 100644 --- a/src/services/api_token.rs +++ b/src/services/api_token.rs @@ -35,8 +35,7 @@ fn scope_to_role(scopes: &[String]) -> String { /// Cached token authentication result #[derive(serde::Serialize, serde::Deserialize)] struct CachedTokenAuth { - user_id: String, - user_int_id: i64, + user_id: i64, role: String, tenant_id: Option, expires_at: Option, @@ -76,7 +75,6 @@ pub fn is_api_token(token: &str) -> bool { #[derive(Debug, serde::Serialize)] pub struct CreateTokenResult { pub id: String, - pub document_id: String, pub name: String, pub token: String, pub token_prefix: String, @@ -112,11 +110,11 @@ pub async fn create_token( let prefix = &plain[..8]; let scopes_json = serde_json::to_string(&scopes).unwrap_or_default(); - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; + let user_id = auth.user_id().ok_or(AppError::Unauthorized)?; let row = api_token::create( pool, - user_int_id, + user_id, name.trim(), &hash, prefix, @@ -126,8 +124,7 @@ pub async fn create_token( .await?; Ok(CreateTokenResult { - id: row.document_id.clone(), - document_id: row.document_id, + id: row.id.to_string(), name: row.name, token: plain, token_prefix: row.token_prefix, @@ -142,11 +139,11 @@ pub async fn list_tokens( pool: &crate::db::Pool, auth: &AuthUser, ) -> AppResult> { - let user_doc_id = auth.ensure_authenticated()?; - let user = crate::models::user::find_by_id(pool, user_doc_id, None) + let user_id = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, user_id, None) .await? .ok_or(AppError::Unauthorized)?; - api_token::list_by_user(pool, user.id).await + api_token::list_by_user(pool, *user.id).await } /// Delete an API token (only the owner or an admin can do this) @@ -156,12 +153,12 @@ pub async fn delete_token( token_id: &str, auth: &AuthUser, ) -> AppResult<()> { - let user_doc_id = auth.ensure_authenticated()?; + let user_id = auth.ensure_authenticated()?; let is_admin = auth.is_admin(); - let token = api_token::find_by_id(pool, token_id) + let token = api_token::find_by_id(pool, crate::utils::id::parse_id(token_id)?) .await? .ok_or_else(|| AppError::NotFound("api_token".into()))?; - let user = crate::models::user::find_by_id(pool, user_doc_id, None) + let user = crate::models::user::find_by_id(pool, user_id, None) .await? .ok_or(AppError::Unauthorized)?; if !is_admin && token.user_id != user.id { @@ -173,7 +170,7 @@ pub async fn delete_token( { tracing::warn!("api_token cache delete on revoke: {e}"); } - api_token::delete_by_id(pool, token_id).await + api_token::delete_by_id(pool, crate::utils::id::parse_id(token_id)?).await } /// Verify an API token and return (user_id, role, tenant_id) @@ -184,7 +181,7 @@ pub async fn verify_api_token( pool: &crate::db::Pool, cache: &dyn CacheStore, plain: &str, -) -> AppResult<(String, i64, String, Option)> { +) -> AppResult<(i64, String, Option)> { let hash = hash_token(plain); let cache_key = format!("{CACHE_PREFIX}{hash}"); @@ -197,7 +194,7 @@ pub async fn verify_api_token( let _ = cache.delete(&cache_key).await; return Err(AppError::Unauthorized); } - return Ok((auth.user_id, auth.user_int_id, auth.role, auth.tenant_id)); + return Ok((auth.user_id, auth.role, auth.tenant_id)); } let token = api_token::find_by_hash(pool, &hash) @@ -207,7 +204,7 @@ pub async fn verify_api_token( if let Some(ref exp) = token.expires_at && exp < &chrono::Utc::now() { - if let Err(e) = api_token::delete_by_id(pool, &token.document_id).await { + if let Err(e) = api_token::delete_by_id(pool, *token.id).await { tracing::warn!("api_token delete expired: {e}"); } if let Err(e) = cache.delete(&cache_key).await { @@ -216,20 +213,19 @@ pub async fn verify_api_token( return Err(AppError::Unauthorized); } - let user = crate::models::user::find_by_pk(pool, token.user_id, None) + let user = crate::models::user::find_by_id(pool, *token.user_id, None) .await? .ok_or(AppError::Unauthorized)?; let scopes: Vec = serde_json::from_str(&token.scopes).unwrap_or_default(); let role = scope_to_role(&scopes); - if let Err(e) = api_token::touch_last_used(pool, token.id).await { + if let Err(e) = api_token::touch_last_used(pool, *token.id).await { tracing::debug!("api_token touch_last_used: {e}"); } let cached_auth = CachedTokenAuth { - user_id: user.document_id.clone(), - user_int_id: user.id, + user_id: *user.id, role: role.clone(), tenant_id: user.tenant_id.clone(), expires_at: token.expires_at, @@ -240,7 +236,7 @@ pub async fn verify_api_token( tracing::debug!("api_token cache set: {e}"); } - Ok((user.document_id, user.id, role, user.tenant_id)) + Ok((*user.id, role, user.tenant_id)) } #[cfg(test)] @@ -262,14 +258,14 @@ mod tests { let user = crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - crate::models::user::update_role(pool, &user.document_id, role, None) + crate::models::user::update_role(pool, *user.id, role, None) .await .unwrap() } @@ -340,22 +336,7 @@ mod tests { async fn create_token_rejects_empty_name() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "u1", - crate::models::user::UserRole::Author, - "default", - ); - let msg = create_token(&pool, &auth, "", vec!["read".into()], None) - .await - .unwrap_err() - .to_string(); - assert!(msg.contains("name is required"), "got: {msg}"); - } - - #[tokio::test] - async fn create_token_rejects_whitespace_name() { - let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - let auth = crate::middleware::auth::AuthUser::new_test( - "u1", + 1, crate::models::user::UserRole::Author, "default", ); @@ -370,7 +351,7 @@ mod tests { async fn create_token_rejects_empty_scopes() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "u1", + 1, crate::models::user::UserRole::Author, "default", ); @@ -385,7 +366,7 @@ mod tests { async fn create_token_rejects_invalid_scope() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "u1", + 1, crate::models::user::UserRole::Author, "default", ); @@ -400,7 +381,7 @@ mod tests { async fn create_token_rejects_mixed_valid_invalid_scope() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "u1", + 1, crate::models::user::UserRole::Author, "default", ); @@ -422,8 +403,7 @@ mod tests { let pool = setup_pool().await; let user = insert_user(&pool, crate::models::user::UserRole::Author).await; let auth = crate::middleware::auth::AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), + Some(*user.id), crate::models::user::UserRole::Author, Some("default".to_string()), ); @@ -452,7 +432,7 @@ mod tests { let past = "2000-01-01T00:00:00+00:00"; crate::models::api_token::create( &pool, - user.id, + *user.id, "Expired", &token_hash, &plain[..8], @@ -480,7 +460,7 @@ mod tests { let future = "2099-12-31T23:59:59+00:00"; crate::models::api_token::create( &pool, - user.id, + *user.id, "Valid", &token_hash, &plain[..8], @@ -490,9 +470,8 @@ mod tests { .await .unwrap(); let cache = test_cache(); - let (uid, _user_int_id, role, tenant_id) = - verify_api_token(&pool, &*cache, plain).await.unwrap(); - assert_eq!(uid, user.document_id); + let (uid, role, tenant_id) = verify_api_token(&pool, &*cache, plain).await.unwrap(); + assert_eq!(uid, *user.id); assert_eq!(role, "admin"); assert_eq!(tenant_id, Some("default".to_string())); assert_eq!(tenant_id, Some("default".to_string())); @@ -506,7 +485,7 @@ mod tests { let token_hash = hash_token(plain); crate::models::api_token::create( &pool, - user.id, + *user.id, "TouchTest", &token_hash, &plain[..8], @@ -541,7 +520,7 @@ mod tests { let user = insert_user(&pool, crate::models::user::UserRole::Reader).await; let row = crate::models::api_token::create( &pool, - user.id, + *user.id, "Own", "h-del", "rblog_d", @@ -551,16 +530,16 @@ mod tests { .await .unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - &user.document_id, + *user.id, crate::models::user::UserRole::Reader, "default", ); let cache = test_cache(); - delete_token(&pool, &*cache, &row.document_id, &auth) + delete_token(&pool, &*cache, &row.id.to_string(), &auth) .await .unwrap(); assert!( - crate::models::api_token::find_by_id(&pool, &row.document_id) + crate::models::api_token::find_by_id(&pool, *row.id) .await .unwrap() .is_none() @@ -574,7 +553,7 @@ mod tests { let admin = insert_user(&pool, crate::models::user::UserRole::Admin).await; let row = crate::models::api_token::create( &pool, - user.id, + *user.id, "Target", "h-adm", "rblog_a", @@ -584,12 +563,12 @@ mod tests { .await .unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - &admin.document_id, + *admin.id, crate::models::user::UserRole::Admin, "default", ); let cache = test_cache(); - delete_token(&pool, &*cache, &row.document_id, &auth) + delete_token(&pool, &*cache, &row.id.to_string(), &auth) .await .unwrap(); } @@ -600,7 +579,7 @@ mod tests { let user = insert_user(&pool, crate::models::user::UserRole::Reader).await; let row = crate::models::api_token::create( &pool, - user.id, + *user.id, "FB", "h-fb", "rblog_f", @@ -610,13 +589,13 @@ mod tests { .await .unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "other-user", + 99999, crate::models::user::UserRole::Reader, "default", ); let cache = test_cache(); assert!( - delete_token(&pool, &*cache, &row.document_id, &auth) + delete_token(&pool, &*cache, &row.id.to_string(), &auth) .await .is_err() ); @@ -626,7 +605,7 @@ mod tests { async fn delete_token_nonexistent_not_found() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); let auth = crate::middleware::auth::AuthUser::new_test( - "user", + 0, crate::models::user::UserRole::Reader, "default", ); @@ -646,7 +625,7 @@ mod tests { let token_hash = hash_token(plain); crate::models::api_token::create( &pool, - user.id, + *user.id, "Cached", &token_hash, &plain[..8], @@ -657,25 +636,25 @@ mod tests { .unwrap(); let cache = test_cache(); - let (uid1, _, _, _) = verify_api_token(&pool, &*cache, plain).await.unwrap(); - assert_eq!(uid1, user.document_id); + let (uid1, _, _) = verify_api_token(&pool, &*cache, plain).await.unwrap(); + assert_eq!(uid1, *user.id); let cache_key = format!("{CACHE_PREFIX}{token_hash}"); assert!(cache.get(&cache_key).await.is_some()); crate::models::api_token::delete_by_id( &pool, - &crate::models::api_token::find_by_hash(&pool, &token_hash) + *crate::models::api_token::find_by_hash(&pool, &token_hash) .await .unwrap() .unwrap() - .document_id, + .id, ) .await .unwrap(); - let (uid2, _, _, _) = verify_api_token(&pool, &*cache, plain).await.unwrap(); - assert_eq!(uid2, user.document_id); + let (uid2, _, _) = verify_api_token(&pool, &*cache, plain).await.unwrap(); + assert_eq!(uid2, *user.id); } #[tokio::test] @@ -687,7 +666,7 @@ mod tests { let past = "2000-01-01T00:00:00+00:00"; crate::models::api_token::create( &pool, - user.id, + *user.id, "CacheExp", &token_hash, &plain[..8], @@ -700,8 +679,7 @@ mod tests { let cache = test_cache(); let cache_key = format!("{CACHE_PREFIX}{token_hash}"); let cached = serde_json::to_string(&CachedTokenAuth { - user_id: user.document_id.clone(), - user_int_id: user.id, + user_id: *user.id, role: "reader".into(), tenant_id: Some("default".to_string()), expires_at: Some(past.parse().unwrap()), diff --git a/src/services/audit.rs b/src/services/audit.rs index 76d43b09..25febb1e 100644 --- a/src/services/audit.rs +++ b/src/services/audit.rs @@ -29,12 +29,11 @@ impl AuditService { ip_address: Option<&str>, user_agent: Option<&str>, ) -> AppResult<()> { - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let entry = AuditEntry { - id: 0, - document_id, + id: crate::utils::id::SnowflakeId(id), tenant_id: Some(tenant_id.to_string()), - actor_id, + actor_id: actor_id.map(crate::utils::id::SnowflakeId), actor_role: actor_role.map(|s| s.to_string()), action: action.to_string(), subject: subject.to_string(), diff --git a/src/services/auth.rs b/src/services/auth.rs index 2e9d1345..2e26d497 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -31,7 +31,6 @@ use crate::models::user_credential::AuthType; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { pub sub: String, - pub iid: i64, pub role: UserRole, #[serde(default = "default_tenant_id")] pub tenant_id: String, @@ -104,8 +103,7 @@ pub fn verify_password(password: &str, hash: &str) -> AppResult { /// Generate an HS256-signed JWT access token. pub(crate) fn generate_access_token_internal( - user_id: &str, - user_int_id: i64, + user_id: i64, role: UserRole, tenant_id: &str, secret: &str, @@ -116,7 +114,6 @@ pub(crate) fn generate_access_token_internal( let exp = (now.timestamp() as usize) + (expires_in as usize); let claims = Claims { sub: user_id.to_string(), - iid: user_int_id, role, tenant_id: tenant_id.to_string(), exp, @@ -154,10 +151,9 @@ pub(crate) fn generate_refresh_token_string_internal() -> AppResult { /// Test helper: generate a JWT token using a fixed secret. #[allow(clippy::doc_lazy_continuation)] #[must_use] -pub fn generate_access_token_for_test(user_id: &str, user_int_id: i64, role: UserRole) -> String { +pub fn generate_access_token_for_test(user_id: i64, role: UserRole) -> String { generate_access_token_internal( user_id, - user_int_id, role, crate::constants::DEFAULT_TENANT, "test-secret-key-at-least-32-characters-long", @@ -192,7 +188,7 @@ pub async fn register( validate_password_strength(&req.password)?; let password_hash = hash_password(&req.password)?; let cred_data = crate::models::user_credential::wrap_password_hash(&password_hash); - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let registered_via = crate::models::user::RegisteredVia::Email; let user = in_transaction!(pool, tx, { @@ -200,7 +196,7 @@ pub async fn register( &mut *tx, "users", [ - "document_id" => &document_id, + "id" => id, "username" => &req.username, "created_at" => now, "updated_at" => now, @@ -213,10 +209,10 @@ pub async fn register( let filter = crate::db::tenant::tenant_filter_ph(tenant_id, 2); let sql = format!( - "SELECT * FROM users WHERE document_id = {}{filter}", + "SELECT * FROM users WHERE id = {}{filter}", crate::db::dialect::ph(1) ); - let mut q = sqlx::query_as::<_, crate::models::user::User>(&sql).bind(&document_id); + let mut q = sqlx::query_as::<_, crate::models::user::User>(&sql).bind(id); if let Some(tid) = tenant_id { q = q.bind(tid); } @@ -224,10 +220,10 @@ pub async fn register( AppError::Internal(anyhow::anyhow!("failed to fetch newly created user")) })?; - let (cred_doc_id, cred_now) = crate::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = crate::utils::id::new_id_and_timestamp(); let verified = if !require_email_verification { 1 } else { 0 }; let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {})", crate::db::dialect::ph(1), crate::db::dialect::ph(2), crate::db::dialect::ph(3), @@ -238,7 +234,7 @@ pub async fn register( crate::db::dialect::ph(8) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(user.id) .bind(AuthType::Email) .bind(&req.email) @@ -258,7 +254,7 @@ pub async fn register( && let Err(e) = crate::services::email_verification::trigger_email_verification( pool, aspect_engine, - user.id, + *user.id, &req.email, ) .await @@ -304,7 +300,7 @@ pub async fn login( return Err(AppError::BadRequest("email_not_verified".into())); } - let user = crate::models::user::find_by_pk(pool, cred.user_id, tenant_id) + let user = crate::models::user::find_by_id(pool, *cred.user_id, tenant_id) .await? .ok_or_else(|| AppError::Unauthorized)?; @@ -313,7 +309,8 @@ pub async fn login( } if let Some(ref tid) = user.tenant_id - && let Ok(Some(tenant)) = crate::models::tenant::find_by_id(pool, tid).await + && let Ok(tid_int) = tid.parse::() + && let Ok(Some(tenant)) = crate::models::tenant::find_by_id(pool, tid_int).await && tenant.status != crate::models::tenant::TenantStatus::Active { return Err(AppError::BadRequest("tenant_disabled".into())); @@ -321,8 +318,7 @@ pub async fn login( let user_role = user.role; let access_token = generate_access_token_internal( - &user.document_id, - user.id, + *user.id, user_role, user.tenant_id .as_deref() @@ -335,7 +331,7 @@ pub async fn login( let expires_at = Utc::now() + chrono::Duration::seconds(jwt_refresh_expires as i64); crate::models::refresh_token::create_token( pool, - user.id, + *user.id, &refresh_token_str, &expires_at.to_rfc3339(), ) @@ -370,7 +366,7 @@ pub async fn refresh( raisfast_derive::check_schema!( "refresh_tokens", "token", - "document_id", + "id", "user_id", "expires_at", "created_at" @@ -384,14 +380,13 @@ pub async fn refresh( return Err(AppError::Unauthorized); } - let user = crate::models::user::find_by_pk(pool, stored.user_id, tenant_id) + let user = crate::models::user::find_by_id(pool, *stored.user_id, tenant_id) .await? .ok_or_else(|| AppError::Unauthorized)?; let user_role = user.role; let access_token = generate_access_token_internal( - &user.document_id, - user.id, + *user.id, user_role, user.tenant_id .as_deref() @@ -402,7 +397,7 @@ pub async fn refresh( let new_refresh_token = generate_refresh_token_string_internal()?; let new_expires_at = Utc::now() + chrono::Duration::seconds(jwt_refresh_expires as i64); let new_expires_str = new_expires_at.to_rfc3339(); - let new_id = uuid::Uuid::now_v7().to_string(); + let new_id = crate::utils::id::new_id(); let now = crate::utils::tz::now_str(); in_transaction!(pool, tx, { @@ -415,14 +410,14 @@ pub async fn refresh( .await?; sqlx::query(&format!( - "INSERT INTO refresh_tokens (document_id, user_id, token, expires_at, created_at) VALUES ({}, {}, {}, {}, {})", + "INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at) VALUES ({}, {}, {}, {}, {})", crate::db::dialect::ph(1), crate::db::dialect::ph(2), crate::db::dialect::ph(3), crate::db::dialect::ph(4), crate::db::dialect::ph(5) )) - .bind(&new_id) + .bind(new_id) .bind(user.id) .bind(&new_refresh_token) .bind(&new_expires_str) @@ -444,11 +439,11 @@ pub async fn refresh( /// /// Deletes all refresh tokens for the user, invalidating sessions across all devices. pub async fn logout(pool: &crate::db::Pool, auth: &AuthUser) -> AppResult<()> { - let user_id = auth.ensure_authenticated()?; - let user = crate::models::user::find_by_id(pool, user_id, auth.tenant_id()) + let uid = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, uid, auth.tenant_id()) .await? .ok_or(AppError::Unauthorized)?; - crate::models::refresh_token::delete_by_user(pool, user.id).await + crate::models::refresh_token::delete_by_user(pool, *user.id).await } /// Change password. @@ -461,13 +456,13 @@ pub async fn change_password( req: UpdatePasswordRequest, ) -> AppResult<()> { raisfast_derive::check_schema!("refresh_tokens", "user_id"); - let user_id = auth.ensure_authenticated()?; + let uid = auth.ensure_authenticated()?; let tenant_id = auth.tenant_id(); - let _user = crate::models::user::find_by_id(pool, user_id, tenant_id) + let _user = crate::models::user::find_by_id(pool, uid, tenant_id) .await? .ok_or_else(|| AppError::not_found("user"))?; - let creds = crate::models::user_credential::find_by_user_id(pool, _user.id).await?; + let creds = crate::models::user_credential::find_by_user_id(pool, *_user.id).await?; let password_cred = creds .iter() .find(|c| c.auth_type == crate::models::user_credential::AuthType::Email) @@ -484,7 +479,7 @@ pub async fn change_password( let new_hash = hash_password(&req.new_password)?; crate::models::user_credential::update_credential_data( pool, - password_cred.id, + *password_cred.id, &crate::models::user_credential::wrap_password_hash(&new_hash), ) .await?; @@ -506,8 +501,8 @@ pub async fn bind_email_credential( email: &str, password: &str, ) -> AppResult<()> { - let user_id = auth.ensure_authenticated()?; - let user = crate::models::user::find_by_id(pool, user_id, auth.tenant_id()) + let uid = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, uid, auth.tenant_id()) .await? .ok_or(AppError::Unauthorized)?; @@ -527,7 +522,7 @@ pub async fn bind_email_credential( crate::models::user_credential::create( pool, - user.id, + *user.id, AuthType::Email, email, &crate::models::user_credential::wrap_password_hash(&hash), @@ -544,8 +539,8 @@ pub async fn delete_credential( auth: &AuthUser, credential_id: i64, ) -> AppResult<()> { - let user_id = auth.ensure_authenticated()?; - let user = crate::models::user::find_by_id(pool, user_id, auth.tenant_id()) + let uid = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, uid, auth.tenant_id()) .await? .ok_or(AppError::Unauthorized)?; @@ -557,7 +552,7 @@ pub async fn delete_credential( return Err(AppError::Forbidden); } - let count = crate::models::user_credential::count_by_user(pool, user.id).await?; + let count = crate::models::user_credential::count_by_user(pool, *user.id).await?; if count <= 1 { return Err(AppError::BadRequest("cannot_remove_last_credential".into())); } @@ -571,12 +566,12 @@ pub async fn list_credentials( pool: &crate::db::Pool, auth: &AuthUser, ) -> AppResult> { - let user_id = auth.ensure_authenticated()?; - let user = crate::models::user::find_by_id(pool, user_id, auth.tenant_id()) + let uid = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, uid, auth.tenant_id()) .await? .ok_or(AppError::Unauthorized)?; - crate::models::user_credential::find_by_user_id(pool, user.id).await + crate::models::user_credential::find_by_user_id(pool, *user.id).await } #[cfg(test)] @@ -623,25 +618,17 @@ mod tests { #[test] fn generate_and_verify_token() { let token = - generate_access_token_internal("user-1", 1, UserRole::Admin, "default", "secret", 900) - .unwrap(); + generate_access_token_internal(1, UserRole::Admin, "default", "secret", 900).unwrap(); let key = jsonwebtoken::DecodingKey::from_secret("secret".as_bytes()); let claims = verify_token(&token, &key).unwrap(); - assert_eq!(claims.sub, "user-1"); + assert_eq!(claims.sub, "1"); assert_eq!(claims.role, UserRole::Admin); } #[test] fn verify_token_rejects_wrong_secret() { - let token = generate_access_token_internal( - "user-1", - 1, - UserRole::Admin, - "default", - "secret-a", - 900, - ) - .unwrap(); + let token = + generate_access_token_internal(1, UserRole::Admin, "default", "secret-a", 900).unwrap(); let key = jsonwebtoken::DecodingKey::from_secret("secret-b".as_bytes()); assert!(verify_token(&token, &key).is_err()); } @@ -650,8 +637,7 @@ mod tests { fn verify_token_rejects_expired() { let now = chrono::Utc::now(); let claims = Claims { - sub: "user-1".into(), - iid: 1, + sub: "1".into(), role: UserRole::Admin, tenant_id: "default".to_string(), exp: (now - chrono::Duration::seconds(120)).timestamp() as usize, @@ -669,7 +655,7 @@ mod tests { #[test] fn generate_test_token_is_valid() { - let token = generate_access_token_for_test("user-1", 1, UserRole::Author); + let token = generate_access_token_for_test(1, UserRole::Author); assert!(token.len() > 20); } } diff --git a/src/services/cart.rs b/src/services/cart.rs index d3395163..f1fe5f82 100644 --- a/src/services/cart.rs +++ b/src/services/cart.rs @@ -20,12 +20,12 @@ pub trait CartService: Send + Sync { attributes: Option, ) -> AppResult<()>; - async fn remove_item(&self, auth: &AuthUser, document_id: &str, user_id: i64) -> AppResult<()>; + async fn remove_item(&self, auth: &AuthUser, id: i64, user_id: i64) -> AppResult<()>; async fn update_quantity( &self, auth: &AuthUser, - document_id: &str, + id: i64, user_id: i64, quantity: i64, ) -> AppResult<()>; @@ -64,10 +64,10 @@ impl CartService for CartServiceImpl { quantity: i64, attributes: Option, ) -> AppResult<()> { - let product = - crate::models::product::find_by_document_id(&self.pool, &product_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let product_id: i64 = crate::utils::id::parse_id(&product_id)?; + let product = crate::models::product::find_by_id(&self.pool, product_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; if product.status != ProductStatus::Active { return Err(AppError::BadRequest("product_not_active".into())); @@ -80,7 +80,7 @@ impl CartService for CartServiceImpl { let existing = crate::models::cart_item::find_by_user_and_product( &self.pool, user_id, - product.id, + *product.id, None, auth.tenant_id(), ) @@ -93,7 +93,7 @@ impl CartService for CartServiceImpl { .ok_or_else(|| AppError::BadRequest("quantity_overflow".into()))?; crate::models::cart_item::update_quantity( &self.pool, - item.id, + *item.id, new_quantity, auth.tenant_id(), ) @@ -108,7 +108,7 @@ impl CartService for CartServiceImpl { crate::models::cart_item::insert( &self.pool, user_id, - product.id, + *product.id, None, quantity, attributes.as_deref(), @@ -120,21 +120,16 @@ impl CartService for CartServiceImpl { Ok(()) } - async fn remove_item(&self, auth: &AuthUser, document_id: &str, user_id: i64) -> AppResult<()> { - let item = crate::models::cart_item::find_by_document_id_opt( - &self.pool, - document_id, - auth.tenant_id(), - ) - .await? - .ok_or_else(|| AppError::not_found("cart_item"))?; + async fn remove_item(&self, auth: &AuthUser, id: i64, user_id: i64) -> AppResult<()> { + let item = crate::models::cart_item::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("cart_item"))?; if item.user_id != user_id { return Err(AppError::Forbidden); } - crate::models::cart_item::delete_by_document_id(&self.pool, document_id, auth.tenant_id()) - .await?; + crate::models::cart_item::delete_by_id(&self.pool, id, auth.tenant_id()).await?; Ok(()) } @@ -142,7 +137,7 @@ impl CartService for CartServiceImpl { async fn update_quantity( &self, auth: &AuthUser, - document_id: &str, + id: i64, user_id: i64, quantity: i64, ) -> AppResult<()> { @@ -150,19 +145,15 @@ impl CartService for CartServiceImpl { return Err(AppError::BadRequest("invalid_quantity".into())); } - let item = crate::models::cart_item::find_by_document_id_opt( - &self.pool, - document_id, - auth.tenant_id(), - ) - .await? - .ok_or_else(|| AppError::not_found("cart_item"))?; + let item = crate::models::cart_item::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("cart_item"))?; if item.user_id != user_id { return Err(AppError::Forbidden); } - crate::models::cart_item::update_quantity(&self.pool, item.id, quantity, auth.tenant_id()) + crate::models::cart_item::update_quantity(&self.pool, *item.id, quantity, auth.tenant_id()) .await?; Ok(()) @@ -178,7 +169,7 @@ impl CartService for CartServiceImpl { for item in &items { let product = - crate::models::product::find_by_id(&self.pool, item.product_id, auth.tenant_id()) + crate::models::product::find_by_id(&self.pool, *item.product_id, auth.tenant_id()) .await?; let (title, price, cover_url) = match product { @@ -190,8 +181,7 @@ impl CartService for CartServiceImpl { total = total.checked_add(line_total).unwrap_or(i64::MAX); response_items.push(CartItemResponse { - id: item.document_id.clone(), - product_id: item.product_id.to_string(), + id: item.id.to_string(), quantity: item.quantity, attributes: item.attributes.clone(), title, @@ -234,7 +224,7 @@ impl CartService for CartServiceImpl { for item in &cart_items { let product = - crate::models::product::find_by_id(&self.pool, item.product_id, auth.tenant_id()) + crate::models::product::find_by_id(&self.pool, *item.product_id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("product"))?; @@ -283,8 +273,8 @@ impl CartService for CartServiceImpl { let mut items = Vec::new(); for (quantity, line_total, product) in &order_items_data { items.push(crate::commands::CreateOrderItemCmd { - order_id: order.id, - product_id: Some(product.id), + order_id: *order.id, + product_id: Some(*product.id), variant_id: None, title: product.title.clone(), description: product.description.clone(), @@ -306,7 +296,7 @@ impl CartService for CartServiceImpl { })?; let items = - crate::models::order_item::find_by_order_id(&self.pool, order.id, auth.tenant_id()) + crate::models::order_item::find_by_order_id(&self.pool, *order.id, auth.tenant_id()) .await?; Ok((order, items)) } diff --git a/src/services/category.rs b/src/services/category.rs index fd3a3cbf..72281ed3 100644 --- a/src/services/category.rs +++ b/src/services/category.rs @@ -9,7 +9,7 @@ use crate::aspects::engine::AspectEngine; use crate::aspects::slug_aspect; use crate::commands::{CreateCategoryCmd, UpdateCategoryCmd}; use crate::dto::{CreateCategoryRequest, UpdateCategoryRequest}; -use crate::errors::app_error::{AppError, AppResult}; +use crate::errors::app_error::AppResult; use crate::middleware::auth::AuthUser; use crate::models::category::Category; @@ -20,11 +20,11 @@ pub trait CategoryService: Send + Sync { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateCategoryRequest, ) -> AppResult; - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()>; - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult; + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()>; + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult; async fn list(&self, auth: &AuthUser) -> AppResult>; async fn list_paginated( &self, @@ -46,13 +46,14 @@ impl CategoryService for CategoryServiceImpl { async fn create(&self, auth: &AuthUser, req: CreateCategoryRequest) -> AppResult { let (req, _d) = self.before_create(auth, req).await?; let slug = slug_aspect::generate_slug(&req.name); - let parent_id = if let Some(ref doc_id) = req.parent_id { - if doc_id.parse::().is_ok() { - doc_id.parse::().ok() + let parent_id = if let Some(ref raw_id) = req.parent_id { + if raw_id.parse::().is_ok() { + raw_id.parse::().ok() } else { - crate::models::category::find_by_document_id(&self.pool, doc_id, auth.tenant_id()) - .await? - .map(|c| c.id) + let pid: i64 = crate::utils::id::parse_id(raw_id)?; + let parent = + crate::models::category::find_by_id(&self.pool, pid, auth.tenant_id()).await?; + Some(*parent.id) } } else { None @@ -65,7 +66,7 @@ impl CategoryService for CategoryServiceImpl { sort_order: req.sort_order.unwrap_or(0), }; let cat = - crate::models::category::create(&self.pool, &cmd, auth.tenant_id(), auth.user_int_id()) + crate::models::category::create(&self.pool, &cmd, auth.tenant_id(), auth.user_id()) .await?; self.after_created(&cat); Ok(cat) @@ -74,13 +75,11 @@ impl CategoryService for CategoryServiceImpl { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateCategoryRequest, ) -> AppResult { let existing = - crate::models::category::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("category"))?; + crate::models::category::find_by_id(&self.pool, id, auth.tenant_id()).await?; let (req, _d) = self.before_update(auth, &existing, req).await?; let new_slug = req .name @@ -88,19 +87,20 @@ impl CategoryService for CategoryServiceImpl { .map(|n| slug_aspect::generate_slug(n)) .unwrap_or(existing.slug); - let parent_id = if let Some(ref doc_id) = req.parent_id { - if doc_id.parse::().is_ok() { - doc_id.parse::().ok() + let parent_id = if let Some(ref raw_id) = req.parent_id { + if raw_id.parse::().is_ok() { + raw_id.parse::().ok() } else { - crate::models::category::find_by_document_id(&self.pool, doc_id, auth.tenant_id()) - .await? - .map(|c| c.id) + let pid: i64 = crate::utils::id::parse_id(raw_id)?; + let parent = + crate::models::category::find_by_id(&self.pool, pid, auth.tenant_id()).await?; + Some(*parent.id) } } else { None }; let cmd = UpdateCategoryCmd { - id: existing.id, + id: *existing.id, name: req.name, slug: Some(new_slug), description: req.description, @@ -108,27 +108,23 @@ impl CategoryService for CategoryServiceImpl { sort_order: req.sort_order, }; let updated = - crate::models::category::update(&self.pool, &cmd, auth.tenant_id(), auth.user_int_id()) + crate::models::category::update(&self.pool, &cmd, auth.tenant_id(), auth.user_id()) .await?; self.after_updated(&updated); Ok(updated) } - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()> { + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()> { let existing = - crate::models::category::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("category"))?; + crate::models::category::find_by_id(&self.pool, id, auth.tenant_id()).await?; self.before_delete(auth, &existing).await?; - crate::models::category::delete(&self.pool, existing.id, auth.tenant_id()).await?; + crate::models::category::delete(&self.pool, *existing.id, auth.tenant_id()).await?; self.after_deleted(&existing); Ok(()) } - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult { - crate::models::category::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("category")) + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult { + crate::models::category::find_by_id(&self.pool, id, auth.tenant_id()).await } async fn list(&self, auth: &AuthUser) -> AppResult> { @@ -156,7 +152,6 @@ mod tests { fn auth(tid: Option<&str>) -> AuthUser { AuthUser::from_parts( - Some("u1".to_string()), Some(1), crate::models::user::UserRole::Admin, tid.map(|s| s.to_string()), @@ -220,7 +215,7 @@ mod tests { let updated = svc .update( &a, - &cat.document_id, + *cat.id, crate::dto::UpdateCategoryRequest { name: Some("New".into()), description: None, @@ -251,7 +246,7 @@ mod tests { ) .await .unwrap(); - svc.delete(&cat.document_id, &a).await.unwrap(); + svc.delete(*cat.id, &a).await.unwrap(); let cats = svc.list(&a).await.unwrap(); assert!(cats.is_empty()); } @@ -261,7 +256,7 @@ mod tests { let pool = setup_pool().await; let svc = make_service(pool.clone()); let a = auth(None); - assert!(svc.delete("nonexistent", &a).await.is_err()); + assert!(svc.delete(999999, &a).await.is_err()); } #[tokio::test] diff --git a/src/services/comment.rs b/src/services/comment.rs index b9825908..2e547984 100644 --- a/src/services/comment.rs +++ b/src/services/comment.rs @@ -42,10 +42,10 @@ pub trait CommentService: Send + Sync { page_size: i64, auth: &AuthUser, ) -> AppResult<(Vec, i64)>; - async fn delete(&self, comment_id: &str, auth: &AuthUser) -> AppResult<()>; + async fn delete(&self, comment_id: i64, auth: &AuthUser) -> AppResult<()>; async fn update_status( &self, - comment_id: &str, + comment_id: i64, status: CommentStatus, auth: &AuthUser, ) -> AppResult<()>; @@ -106,11 +106,9 @@ impl CommentService for CommentServiceImpl { .ok_or_else(|| AppError::not_found("post"))?; if let Some(pid_str) = parent_id { - let pid: i64 = pid_str - .parse() - .map_err(|e| AppError::BadRequest(format!("invalid parent_id: {e}")))?; + let pid: i64 = crate::utils::id::parse_id(pid_str)?; let all_comments = - comment::find_approved_by_post(&self.pool, p.id, auth.tenant_id()).await?; + comment::find_approved_by_post(&self.pool, *p.id, auth.tenant_id()).await?; let parent = all_comments .iter() .find(|c| c.id == pid) @@ -135,15 +133,16 @@ impl CommentService for CommentServiceImpl { .before_create("comments", auth, comment_input) .await?; - let parent_id = if let Some(ref doc_id) = filtered.parent_id { - if doc_id.is_empty() { + let parent_id = if let Some(ref raw_id) = filtered.parent_id { + if raw_id.is_empty() { None - } else if let Ok(int_id) = doc_id.parse::() { + } else if let Ok(int_id) = raw_id.parse::() { Some(int_id) } else { - comment::find_by_document_id(&self.pool, doc_id, auth.tenant_id()) + let cid: i64 = crate::utils::id::parse_id(raw_id)?; + comment::find_by_id(&self.pool, cid, auth.tenant_id()) .await? - .map(|c| c.id) + .map(|c| *c.id) } } else { None @@ -152,8 +151,8 @@ impl CommentService for CommentServiceImpl { let c = comment::create( &self.pool, &CreateCommentCmd { - post_id: p.id, - created_by: auth.user_int_id(), + post_id: *p.id, + created_by: auth.user_id(), nickname: filtered.nickname, email: filtered.email, content: filtered.content, @@ -166,16 +165,15 @@ impl CommentService for CommentServiceImpl { self.aspect_engine.emit(Event::CommentCreated(c.clone())); Ok(CommentResponse { - id: c.id, - document_id: c.document_id.clone(), - post_id: c.post_id, - created_by: c.created_by, + id: c.id.to_string(), nickname: c.nickname, content: c.content, - parent_id: c.parent_id, depth: 0, replies: vec![], created_at: c.created_at, + post_id: None, + created_by: None, + parent_id: None, }) } @@ -192,7 +190,7 @@ impl CommentService for CommentServiceImpl { let (comments, total) = comment::find_approved_by_post_paginated( &self.pool, - p.id, + *p.id, page, page_size, auth.tenant_id(), @@ -201,30 +199,30 @@ impl CommentService for CommentServiceImpl { Ok((comment::build_tree(&comments), total)) } - async fn delete(&self, comment_id: &str, auth: &AuthUser) -> AppResult<()> { - let c = comment::find_by_document_id(&self.pool, comment_id, auth.tenant_id()) + async fn delete(&self, comment_id: i64, auth: &AuthUser) -> AppResult<()> { + let c = comment::find_by_id(&self.pool, comment_id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("comment"))?; self.before_delete(auth, &c).await?; - comment::delete(&self.pool, c.id, auth.tenant_id()).await?; + comment::delete(&self.pool, *c.id, auth.tenant_id()).await?; self.after_deleted(&c); Ok(()) } async fn update_status( &self, - comment_id: &str, + comment_id: i64, status: CommentStatus, auth: &AuthUser, ) -> AppResult<()> { - let c = comment::find_by_document_id(&self.pool, comment_id, auth.tenant_id()) + let c = comment::find_by_id(&self.pool, comment_id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("comment"))?; self.aspect_engine .before_update("comments", auth, &c, status) .await?; - comment::update_status(&self.pool, c.id, status, auth.tenant_id()).await?; + comment::update_status(&self.pool, *c.id, status, auth.tenant_id()).await?; self.after_updated(&c); Ok(()) } @@ -249,37 +247,27 @@ mod tests { } fn auth(user: &crate::models::user::User) -> AuthUser { - AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Admin, - None, - ) + AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Admin, None) } async fn insert_user(pool: &crate::db::Pool) -> crate::models::user::User { let user = crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, ) .await .unwrap(); - crate::models::user::update_role( - pool, - &user.document_id, - crate::models::user::UserRole::Admin, - None, - ) - .await - .unwrap() + crate::models::user::update_role(pool, *user.id, crate::models::user::UserRole::Admin, None) + .await + .unwrap() } async fn insert_post(pool: &crate::db::Pool, user_id: i64) -> i64 { - crate::models::post::create( + *crate::models::post::create( pool, &crate::commands::CreatePostCmd { title: "Test".into(), @@ -325,13 +313,13 @@ mod tests { async fn update_comment_status_valid() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let post_id = insert_post(&pool, user.id).await; - let c = insert_comment(&pool, post_id, user.id).await; + let post_id = insert_post(&pool, *user.id).await; + let c = insert_comment(&pool, post_id, *user.id).await; let svc = CommentServiceImpl::new(Arc::new(pool.clone()), Arc::new(AspectEngine::new())); - svc.update_status(&c.document_id, CommentStatus::Approved, &auth(&user)) + svc.update_status(*c.id, CommentStatus::Approved, &auth(&user)) .await .unwrap(); - let updated = crate::models::comment::find_by_id(&pool, c.id, None) + let updated = crate::models::comment::find_by_id(&pool, *c.id, None) .await .unwrap() .unwrap(); @@ -345,9 +333,9 @@ mod tests { async fn update_comment_status_not_found() { let pool = setup_pool().await; let svc = CommentServiceImpl::new(Arc::new(pool.clone()), Arc::new(AspectEngine::new())); - let a = AuthUser::new_test("any", crate::models::user::UserRole::Admin, ""); + let a = AuthUser::new_test(0, crate::models::user::UserRole::Admin, ""); assert!( - svc.update_status("missing", CommentStatus::Approved, &a) + svc.update_status(999999, CommentStatus::Approved, &a) .await .is_err() ); @@ -357,18 +345,13 @@ mod tests { async fn delete_comment_by_owner() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let post_id = insert_post(&pool, user.id).await; - let c = insert_comment(&pool, post_id, user.id).await; + let post_id = insert_post(&pool, *user.id).await; + let c = insert_comment(&pool, post_id, *user.id).await; let svc = CommentServiceImpl::new(Arc::new(pool.clone()), Arc::new(AspectEngine::new())); - let a = AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Admin, - None, - ); - svc.delete(&c.document_id, &a).await.unwrap(); + let a = AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Admin, None); + svc.delete(*c.id, &a).await.unwrap(); assert!( - crate::models::comment::find_by_id(&pool, c.id, None) + crate::models::comment::find_by_id(&pool, *c.id, None) .await .unwrap() .is_none() @@ -379,21 +362,21 @@ mod tests { async fn delete_comment_not_found() { let pool = setup_pool().await; let svc = CommentServiceImpl::new(Arc::new(pool.clone()), Arc::new(AspectEngine::new())); - let a = AuthUser::new_test("any", crate::models::user::UserRole::Admin, ""); - assert!(svc.delete("missing", &a).await.is_err()); + let a = AuthUser::new_test(0, crate::models::user::UserRole::Admin, ""); + assert!(svc.delete(999999, &a).await.is_err()); } #[tokio::test] async fn update_comment_status_spam() { let pool = setup_pool().await; let user = insert_user(&pool).await; - let post_id = insert_post(&pool, user.id).await; - let c = insert_comment(&pool, post_id, user.id).await; + let post_id = insert_post(&pool, *user.id).await; + let c = insert_comment(&pool, post_id, *user.id).await; let svc = CommentServiceImpl::new(Arc::new(pool.clone()), Arc::new(AspectEngine::new())); - svc.update_status(&c.document_id, CommentStatus::Spam, &auth(&user)) + svc.update_status(*c.id, CommentStatus::Spam, &auth(&user)) .await .unwrap(); - let updated = crate::models::comment::find_by_id(&pool, c.id, None) + let updated = crate::models::comment::find_by_id(&pool, *c.id, None) .await .unwrap() .unwrap(); diff --git a/src/services/content_revision.rs b/src/services/content_revision.rs index 8524f335..ef43c3e5 100644 --- a/src/services/content_revision.rs +++ b/src/services/content_revision.rs @@ -15,7 +15,7 @@ pub struct DiffResult { pub async fn list_revisions( pool: &Pool, content_type: &str, - content_id: &str, + content_id: i64, _page: i64, _page_size: i64, ) -> AppResult<(Vec, i64)> { @@ -36,7 +36,7 @@ pub async fn list_revisions( pub async fn get_revision( pool: &Pool, content_type: &str, - content_id: &str, + content_id: i64, revision_id: i64, ) -> AppResult { content_revision::get_revision(pool, content_type, content_id, revision_id) @@ -47,7 +47,7 @@ pub async fn get_revision( pub async fn restore_revision( pool: &Pool, content_type: &str, - content_id: &str, + content_id: i64, revision_id: i64, ) -> AppResult { let revision = get_revision(pool, content_type, content_id, revision_id).await?; @@ -64,7 +64,7 @@ pub async fn restore_revision( pub async fn diff_revisions( pool: &Pool, content_type: &str, - content_id: &str, + content_id: i64, rev_id_a: i64, rev_id_b: i64, ) -> AppResult { diff --git a/src/services/email_verification.rs b/src/services/email_verification.rs index bad0a93e..aebeac78 100644 --- a/src/services/email_verification.rs +++ b/src/services/email_verification.rs @@ -96,7 +96,7 @@ pub async fn resend_verification( return Err(AppError::BadRequest("email_already_verified".into())); } - trigger_email_verification(pool, aspect_engine, cred.user_id, &cred.identifier).await + trigger_email_verification(pool, aspect_engine, *cred.user_id, &cred.identifier).await } #[cfg(test)] @@ -124,7 +124,7 @@ mod tests { .unwrap(); crate::models::user_credential::create( pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Email, email, &crate::models::user_credential::wrap_password_hash("hash"), @@ -140,11 +140,11 @@ mod tests { let pool = setup_pool().await; let user = insert_user(&pool, "verify@test.com").await; let ae = aspect_engine(); - super::trigger_email_verification(&pool, &ae, user.id, "verify@test.com") + super::trigger_email_verification(&pool, &ae, *user.id, "verify@test.com") .await .unwrap(); let row = - crate::models::email_verification::create(&pool, user.id, "verify@test.com", 3600) + crate::models::email_verification::create(&pool, *user.id, "verify@test.com", 3600) .await .unwrap(); let found = crate::models::email_verification::find_by_token(&pool, &row.token) @@ -158,7 +158,7 @@ mod tests { let pool = setup_pool().await; let user = insert_user(&pool, "replace@test.com").await; let ae = aspect_engine(); - super::trigger_email_verification(&pool, &ae, user.id, "replace@test.com") + super::trigger_email_verification(&pool, &ae, *user.id, "replace@test.com") .await .unwrap(); let sql = format!( @@ -171,7 +171,7 @@ mod tests { .await .unwrap(); assert_eq!(count_before, 1); - super::trigger_email_verification(&pool, &ae, user.id, "replace@test.com") + super::trigger_email_verification(&pool, &ae, *user.id, "replace@test.com") .await .unwrap(); let (count_after,): (i64,) = sqlx::query_as(&sql) @@ -187,7 +187,7 @@ mod tests { let pool = setup_pool().await; let user = insert_user(&pool, "v@test.com").await; let ae = aspect_engine(); - super::trigger_email_verification(&pool, &ae, user.id, "v@test.com") + super::trigger_email_verification(&pool, &ae, *user.id, "v@test.com") .await .unwrap(); let sql = format!( @@ -246,7 +246,7 @@ mod tests { let pool = setup_pool().await; let user = insert_user(&pool, "verified@test.com").await; let ae = aspect_engine(); - super::trigger_email_verification(&pool, &ae, user.id, "verified@test.com") + super::trigger_email_verification(&pool, &ae, *user.id, "verified@test.com") .await .unwrap(); let sql = format!( diff --git a/src/services/media.rs b/src/services/media.rs index 990037d4..74bdc689 100644 --- a/src/services/media.rs +++ b/src/services/media.rs @@ -166,7 +166,7 @@ pub async fn save_file( let media = media::create( pool, &CreateMediaCmd { - user_id: user.id, + user_id: *user.id, filename: filename.to_string(), filepath: key.clone(), mimetype: content_type.to_string(), @@ -212,7 +212,7 @@ pub async fn list( let user = crate::models::user::find_by_id(pool, user_id, auth.tenant_id()) .await? .ok_or(AppError::Unauthorized)?; - media::find_all(pool, user.id, page, page_size, auth.tenant_id()).await + media::find_all(pool, *user.id, page, page_size, auth.tenant_id()).await } pub async fn admin_list( @@ -231,7 +231,7 @@ pub async fn admin_delete_media( auth: &AuthUser, ) -> AppResult<()> { let sql = format!( - "SELECT id FROM media WHERE document_id = {}{}", + "SELECT id FROM media WHERE id = {}{}", crate::db::dialect::ph(1), crate::db::tenant::tenant_filter_ph(auth.tenant_id(), 2) ); @@ -275,7 +275,7 @@ pub async fn delete_media( .ok_or(AppError::Unauthorized)?; let sql2 = format!( - "SELECT id FROM media WHERE document_id = {}{}", + "SELECT id FROM media WHERE id = {}{}", crate::db::dialect::ph(1), crate::db::tenant::tenant_filter_ph(auth.tenant_id(), 2) ); @@ -313,7 +313,7 @@ pub async fn stats(pool: &crate::db::Pool, auth: &AuthUser) -> AppResult AppResult { - let current_user_id = auth.user_id(); let provider = registry.get(provider_name).ok_or_else(|| { AppError::BadRequest(format!("unsupported OAuth provider: {provider_name}")) })?; @@ -38,15 +37,8 @@ pub async fn initiate_oauth( let code_challenge = crate::oauth::generate_code_challenge(&code_verifier); let expires_at = (Utc::now() + chrono::Duration::minutes(10)).to_rfc3339(); - let resolved_user_id = if let Some(uid_str) = current_user_id { - let user = crate::models::user::find_by_id(pool, uid_str, None) - .await? - .ok_or(AppError::Unauthorized)?; - Some(user.id) - } else { - None - }; - let state_doc_id = oauth::create_state( + let resolved_user_id = auth.user_id(); + let state_id = oauth::create_state( pool, provider_name, &code_verifier, @@ -54,8 +46,8 @@ pub async fn initiate_oauth( &expires_at, ) .await?; - - Ok(provider.authorize_url(&state_doc_id, &code_challenge)) + let state_str = state_id.to_string(); + Ok(provider.authorize_url(&state_str, &code_challenge)) } /// OAuth callback result @@ -94,7 +86,8 @@ pub async fn handle_callback( AppError::BadRequest(format!("unsupported OAuth provider: {provider_name}")) })?; - let oauth_state = oauth::consume_state(pool, state) + let state_id: i64 = crate::utils::id::parse_id(state)?; + let oauth_state = oauth::consume_state(pool, state_id) .await? .ok_or_else(|| AppError::BadRequest("invalid or expired OAuth state".into()))?; @@ -113,7 +106,7 @@ pub async fn handle_callback( oauth::find_by_provider_user(pool, provider_name, &user_info.provider_user_id).await?; if let Some(account) = existing { - let user = crate::models::user::find_by_pk(pool, account.user_id, None) + let user = crate::models::user::find_by_id(pool, *account.user_id, None) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("OAuth bound user not found")))?; @@ -126,15 +119,15 @@ pub async fn handle_callback( ) .await?; - update_oauth_account(pool, account.id, &token_resp, &user_info).await?; + update_oauth_account(pool, *account.id, &token_resp, &user_info).await?; return Ok(OAuthCallbackResult::LoginSuccess(Box::new(login_resp))); } if let Some(bind_user_id) = oauth_state.user_id { - do_bind_oauth(pool, bind_user_id, provider_name, &token_resp, &user_info).await?; + do_bind_oauth(pool, *bind_user_id, provider_name, &token_resp, &user_info).await?; - let user = crate::models::user::find_by_pk(pool, bind_user_id, None) + let user = crate::models::user::find_by_id(pool, *bind_user_id, None) .await? .ok_or_else(|| AppError::not_found("user"))?; @@ -158,10 +151,10 @@ pub async fn handle_callback( ) .await?; if let Some(cred) = cred { - let user = crate::models::user::find_by_pk(pool, cred.user_id, None) + let user = crate::models::user::find_by_id(pool, *cred.user_id, None) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("user not found")))?; - do_bind_oauth(pool, user.id, provider_name, &token_resp, &user_info).await?; + do_bind_oauth(pool, *user.id, provider_name, &token_resp, &user_info).await?; let login_resp = create_login_response_for_user( &user, @@ -183,7 +176,7 @@ pub async fn handle_callback( let user = auto_register_user(pool, provider_name, &user_info, aspect_engine).await?; - do_bind_oauth(pool, user.id, provider_name, &token_resp, &user_info).await?; + do_bind_oauth(pool, *user.id, provider_name, &token_resp, &user_info).await?; let login_resp = create_login_response_for_user( &user, @@ -208,22 +201,22 @@ pub async fn unbind_oauth( .await? .ok_or_else(|| AppError::not_found("user"))?; - let cred_count = crate::models::user_credential::count_by_user(pool, user.id).await?; + let cred_count = crate::models::user_credential::count_by_user(pool, *user.id).await?; if cred_count <= 1 { return Err(AppError::BadRequest( "cannot unbind: this is the only login method".into(), )); } - let deleted = oauth::delete_account(pool, user.id, provider_name).await?; + let deleted = oauth::delete_account(pool, *user.id, provider_name).await?; if !deleted { return Err(AppError::not_found("oauth binding")); } - let creds = crate::models::user_credential::find_by_user_id(pool, user.id).await?; + let creds = crate::models::user_credential::find_by_user_id(pool, *user.id).await?; for cred in creds { if cred.auth_type == crate::models::user_credential::AuthType::Oauth { - crate::models::user_credential::delete_by_id(pool, cred.id).await?; + crate::models::user_credential::delete_by_id(pool, *cred.id).await?; break; } } @@ -240,7 +233,7 @@ pub async fn list_bindings( let user = crate::models::user::find_by_id(pool, user_id, None) .await? .ok_or(AppError::Unauthorized)?; - let accounts = oauth::find_by_user_id(pool, user.id).await?; + let accounts = oauth::find_by_user_id(pool, *user.id).await?; Ok(accounts .into_iter() .map(|a| OAuthBindingInfo { @@ -274,8 +267,7 @@ async fn create_login_response_for_user( ) -> AppResult { let user_role = user.role; let access_token = crate::services::auth::generate_access_token_internal( - &user.document_id, - user.id, + *user.id, user_role, user.tenant_id .as_deref() @@ -289,7 +281,7 @@ async fn create_login_response_for_user( crate::models::refresh_token::create_token( pool, - user.id, + *user.id, &refresh_token_str, &expires_at.to_rfc3339(), ) @@ -350,7 +342,7 @@ async fn auto_register_user( if !email.is_empty() { crate::models::user_credential::create( pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Email, &email, "", @@ -359,7 +351,7 @@ async fn auto_register_user( .await?; } - let user = crate::models::user::find_by_pk(pool, user.id, None) + let user = crate::models::user::find_by_id(pool, *user.id, None) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch created user")))?; @@ -441,7 +433,7 @@ async fn do_bind_oauth( oauth::create_account( pool, oauth::CreateOAuthAccountParams { - user_id, + user_id: crate::utils::id::SnowflakeId(user_id), provider: provider_name, provider_user_id: &user_info.provider_user_id, email: user_info.email.as_deref(), @@ -490,7 +482,7 @@ async fn update_oauth_account( oauth::update_account( pool, oauth::UpdateOAuthAccountParams { - id: account_id, + id: crate::utils::id::SnowflakeId(account_id), email: user_info.email.as_deref(), display_name: user_info.display_name.as_deref(), avatar_url: user_info.avatar_url.as_deref(), diff --git a/src/services/order.rs b/src/services/order.rs index c4a5c9e5..94b98d65 100644 --- a/src/services/order.rs +++ b/src/services/order.rs @@ -23,14 +23,13 @@ pub trait OrderService: Send + Sync { user_id: i64, req: CreateOrderRequest, ) -> AppResult<(Order, Vec)>; - async fn cancel(&self, auth: &AuthUser, order_id: &str, user_id: i64) -> AppResult<()>; - async fn mark_paid(&self, auth: &AuthUser, order_id: &str) -> AppResult; - async fn ship(&self, auth: &AuthUser, order_id: &str, req: &ShipOrderRequest) -> AppResult<()>; - async fn confirm_receipt(&self, auth: &AuthUser, order_id: &str, user_id: i64) - -> AppResult<()>; - async fn refund(&self, auth: &AuthUser, order_id: &str) -> AppResult<()>; - async fn admin_cancel(&self, auth: &AuthUser, order_id: &str) -> AppResult<()>; - async fn get(&self, auth: &AuthUser, order_id: &str) -> AppResult<(Order, Vec)>; + async fn cancel(&self, auth: &AuthUser, order_id: i64, user_id: i64) -> AppResult<()>; + async fn mark_paid(&self, auth: &AuthUser, order_id: i64) -> AppResult; + async fn ship(&self, auth: &AuthUser, order_id: i64, req: &ShipOrderRequest) -> AppResult<()>; + async fn confirm_receipt(&self, auth: &AuthUser, order_id: i64, user_id: i64) -> AppResult<()>; + async fn refund(&self, auth: &AuthUser, order_id: i64) -> AppResult<()>; + async fn admin_cancel(&self, auth: &AuthUser, order_id: i64) -> AppResult<()>; + async fn get(&self, auth: &AuthUser, order_id: i64) -> AppResult<(Order, Vec)>; async fn list_user( &self, auth: &AuthUser, @@ -48,7 +47,7 @@ pub trait OrderService: Send + Sync { async fn update_admin_remark( &self, auth: &AuthUser, - order_id: &str, + order_id: i64, admin_remark: &str, ) -> AppResult<()>; async fn get_stats(&self, auth: &AuthUser) -> AppResult; @@ -122,13 +121,11 @@ impl OrderService for OrderServiceImpl { if item.quantity > MAX_QUANTITY { return Err(AppError::BadRequest("quantity_exceeds_limit".into())); } - let product = crate::models::product::find_by_document_id( - &self.pool, - &item.product_id, - auth.tenant_id(), - ) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let product_id: i64 = crate::utils::id::parse_id(&item.product_id)?; + let product = + crate::models::product::find_by_id(&self.pool, product_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; if product.status != ProductStatus::Active { return Err(AppError::BadRequest("product_not_active".into())); @@ -177,8 +174,8 @@ impl OrderService for OrderServiceImpl { let mut items = Vec::new(); for (quantity, line_total, product) in &order_items_data { items.push(crate::commands::CreateOrderItemCmd { - order_id: order.id, - product_id: Some(product.id), + order_id: *order.id, + product_id: Some(*product.id), variant_id: None, title: product.title.clone(), description: product.description.clone(), @@ -198,16 +195,15 @@ impl OrderService for OrderServiceImpl { self.after_created(&order); let items = - crate::models::order_item::find_by_order_id(&self.pool, order.id, auth.tenant_id()) + crate::models::order_item::find_by_order_id(&self.pool, *order.id, auth.tenant_id()) .await?; Ok((order, items)) } - async fn cancel(&self, auth: &AuthUser, order_id: &str, user_id: i64) -> AppResult<()> { - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + async fn cancel(&self, auth: &AuthUser, order_id: i64, user_id: i64) -> AppResult<()> { + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.user_id != user_id { return Err(AppError::Forbidden); @@ -224,7 +220,7 @@ impl OrderService for OrderServiceImpl { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_status_cas( &mut tx, - order.id, + *order.id, OrderStatus::Cancelled, Some("cancelled_at"), OrderStatus::Pending, @@ -243,12 +239,11 @@ impl OrderService for OrderServiceImpl { Ok(()) } - async fn mark_paid(&self, auth: &AuthUser, order_id: &str) -> AppResult { + async fn mark_paid(&self, auth: &AuthUser, order_id: i64) -> AppResult { auth.ensure_admin()?; - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.status != OrderStatus::Pending { return Err(AppError::BadRequest("only_pending_can_pay".into())); @@ -262,7 +257,7 @@ impl OrderService for OrderServiceImpl { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_status_cas( &mut tx, - order.id, + *order.id, OrderStatus::Paid, Some("paid_at"), OrderStatus::Pending, @@ -277,7 +272,7 @@ impl OrderService for OrderServiceImpl { .await; result?; - let paid = crate::models::order::find_by_id(&self.pool, order.id, auth.tenant_id()) + let paid = crate::models::order::find_by_id(&self.pool, *order.id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("order"))?; @@ -285,12 +280,11 @@ impl OrderService for OrderServiceImpl { Ok(paid) } - async fn ship(&self, auth: &AuthUser, order_id: &str, req: &ShipOrderRequest) -> AppResult<()> { + async fn ship(&self, auth: &AuthUser, order_id: i64, req: &ShipOrderRequest) -> AppResult<()> { auth.ensure_admin()?; - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.status != OrderStatus::Paid { return Err(AppError::BadRequest("only_paid_can_ship".into())); @@ -300,7 +294,7 @@ impl OrderService for OrderServiceImpl { .before_update("orders", auth, &order, OrderStatus::Shipped) .await?; - let order_id = order.id; + let order_id = *order.id; let result: Result<(), AppError> = async { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_shipped( @@ -323,16 +317,10 @@ impl OrderService for OrderServiceImpl { Ok(()) } - async fn confirm_receipt( - &self, - auth: &AuthUser, - order_id: &str, - user_id: i64, - ) -> AppResult<()> { - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + async fn confirm_receipt(&self, auth: &AuthUser, order_id: i64, user_id: i64) -> AppResult<()> { + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.user_id != user_id { return Err(AppError::Forbidden); @@ -349,7 +337,7 @@ impl OrderService for OrderServiceImpl { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_status_cas( &mut tx, - order.id, + *order.id, OrderStatus::Completed, Some("completed_at"), OrderStatus::Shipped, @@ -368,12 +356,11 @@ impl OrderService for OrderServiceImpl { Ok(()) } - async fn refund(&self, auth: &AuthUser, order_id: &str) -> AppResult<()> { + async fn refund(&self, auth: &AuthUser, order_id: i64) -> AppResult<()> { auth.ensure_admin()?; - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.status != OrderStatus::Paid && order.status != OrderStatus::Shipped { return Err(AppError::BadRequest( @@ -390,7 +377,7 @@ impl OrderService for OrderServiceImpl { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_status_cas( &mut tx, - order.id, + *order.id, OrderStatus::Refunding, Some("refunding_at"), expected, @@ -406,12 +393,11 @@ impl OrderService for OrderServiceImpl { result } - async fn admin_cancel(&self, auth: &AuthUser, order_id: &str) -> AppResult<()> { + async fn admin_cancel(&self, auth: &AuthUser, order_id: i64) -> AppResult<()> { auth.ensure_admin()?; - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if order.status != OrderStatus::Pending && order.status != OrderStatus::Paid { return Err(AppError::BadRequest( @@ -428,7 +414,7 @@ impl OrderService for OrderServiceImpl { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::order::tx_update_status_cas( &mut tx, - order.id, + *order.id, OrderStatus::Cancelled, Some("cancelled_at"), expected, @@ -446,19 +432,18 @@ impl OrderService for OrderServiceImpl { Ok(()) } - async fn get(&self, auth: &AuthUser, order_id: &str) -> AppResult<(Order, Vec)> { - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + async fn get(&self, auth: &AuthUser, order_id: i64) -> AppResult<(Order, Vec)> { + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; if auth.role() != "admin" { - let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?; - if order.user_id != user_int_id { + let user_id = auth.user_id().ok_or(AppError::Unauthorized)?; + if order.user_id != user_id { return Err(AppError::Forbidden); } } let items = - crate::models::order_item::find_by_order_id(&self.pool, order.id, auth.tenant_id()) + crate::models::order_item::find_by_order_id(&self.pool, *order.id, auth.tenant_id()) .await?; Ok((order, items)) } @@ -481,7 +466,7 @@ impl OrderService for OrderServiceImpl { let mut result = Vec::with_capacity(orders.len()); for o in orders { let items = - crate::models::order_item::find_by_order_id(&self.pool, o.id, auth.tenant_id()) + crate::models::order_item::find_by_order_id(&self.pool, *o.id, auth.tenant_id()) .await?; result.push((o, items)); } @@ -507,7 +492,7 @@ impl OrderService for OrderServiceImpl { let mut result = Vec::with_capacity(orders.len()); for o in orders { let items = - crate::models::order_item::find_by_order_id(&self.pool, o.id, auth.tenant_id()) + crate::models::order_item::find_by_order_id(&self.pool, *o.id, auth.tenant_id()) .await?; result.push((o, items)); } @@ -517,17 +502,16 @@ impl OrderService for OrderServiceImpl { async fn update_admin_remark( &self, auth: &AuthUser, - order_id: &str, + order_id: i64, admin_remark: &str, ) -> AppResult<()> { auth.ensure_admin()?; - let order = - crate::models::order::find_by_document_id(&self.pool, order_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("order"))?; + let order = crate::models::order::find_by_id(&self.pool, order_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("order"))?; crate::models::order::update_admin_remark( &self.pool, - order.id, + *order.id, admin_remark, auth.tenant_id(), ) @@ -558,7 +542,6 @@ mod tests { fn auth(tid: Option<&str>) -> AuthUser { AuthUser::from_parts( - Some("u1".to_string()), Some(1), crate::models::user::UserRole::Admin, tid.map(|s| s.to_string()), @@ -568,7 +551,6 @@ mod tests { #[allow(dead_code)] fn auth_with_id(user_int_id: i64) -> AuthUser { AuthUser::from_parts( - Some(format!("u{user_int_id}")), Some(user_int_id), crate::models::user::UserRole::Reader, None, @@ -576,19 +558,14 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") - .bind(&doc_id) + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')") + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -637,16 +614,16 @@ mod tests { .execute(pool) .await .unwrap(); - crate::models::product::find_by_id(pool, p.id, None) + crate::models::product::find_by_id(pool, *p.id, None) .await .unwrap() .unwrap() } - fn make_create_req(prod_doc_id: &str, quantity: i64) -> CreateOrderRequest { + fn make_create_req(prod_id: &str, quantity: i64) -> CreateOrderRequest { CreateOrderRequest { items: vec![CreateOrderItemRequest { - product_id: prod_doc_id.to_string(), + product_id: prod_id.to_string(), quantity, }], currency: None, @@ -666,7 +643,7 @@ mod tests { let uid = seed_user(pool).await; let prod = seed_active_product(pool, "Widget", 1000).await; let (order, _) = svc - .create(auth, uid, make_create_req(&prod.document_id, 1)) + .create(auth, uid, make_create_req(&prod.id.to_string(), 1)) .await .unwrap(); (uid, order) @@ -681,7 +658,7 @@ mod tests { let prod = seed_active_product(&pool, "Widget", 1000).await; let (order, items) = svc - .create(&a, uid, make_create_req(&prod.document_id, 2)) + .create(&a, uid, make_create_req(&prod.id.to_string(), 2)) .await .unwrap(); @@ -714,11 +691,11 @@ mod tests { CreateOrderRequest { items: vec![ CreateOrderItemRequest { - product_id: p1.document_id.clone(), + product_id: p1.id.to_string(), quantity: 3, }, CreateOrderItemRequest { - product_id: p2.document_id.clone(), + product_id: p2.id.to_string(), quantity: 1, }, ], @@ -779,7 +756,7 @@ mod tests { uid, CreateOrderRequest { items: vec![CreateOrderItemRequest { - product_id: "nonexistent".into(), + product_id: "99999999".into(), quantity: 1, }], currency: None, @@ -839,7 +816,7 @@ mod tests { .unwrap(); let err = svc - .create(&a, uid, make_create_req(&draft_product.document_id, 1)) + .create(&a, uid, make_create_req(&draft_product.id.to_string(), 1)) .await .unwrap_err(); assert!(matches!(err, AppError::BadRequest(ref s) if s == "product_not_active")); @@ -852,8 +829,8 @@ mod tests { let a = auth(None); let (uid, order) = seed_order(svc.as_ref(), &pool, &a).await; - svc.cancel(&a, &order.document_id, uid).await.unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + svc.cancel(&a, *order.id, uid).await.unwrap(); + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -868,7 +845,7 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - let err = svc.cancel(&a, &order.document_id, 999).await.unwrap_err(); + let err = svc.cancel(&a, *order.id, 999).await.unwrap_err(); assert!(matches!(err, AppError::Forbidden)); } @@ -879,11 +856,11 @@ mod tests { let a = auth(None); let (uid, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool, order.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *order.id, "paid", Some("paid_at"), None) .await .unwrap(); - let err = svc.cancel(&a, &order.document_id, uid).await.unwrap_err(); + let err = svc.cancel(&a, *order.id, uid).await.unwrap_err(); assert!(matches!(err, AppError::BadRequest(ref s) if s == "only_pending_can_cancel")); } @@ -894,7 +871,7 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - let paid = svc.mark_paid(&a, &order.document_id).await.unwrap(); + let paid = svc.mark_paid(&a, *order.id).await.unwrap(); assert_eq!(paid.status, OrderStatus::Paid); assert!(paid.paid_at.is_some()); } @@ -908,7 +885,7 @@ mod tests { crate::models::order::update_status( &pool, - order.id, + *order.id, "cancelled", Some("cancelled_at"), None, @@ -916,7 +893,7 @@ mod tests { .await .unwrap(); - let err = svc.mark_paid(&a, &order.document_id).await.unwrap_err(); + let err = svc.mark_paid(&a, *order.id).await.unwrap_err(); assert!(matches!(err, AppError::BadRequest(ref s) if s == "only_pending_can_pay")); } @@ -927,12 +904,18 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool.clone(), order.id, "paid", Some("paid_at"), None) - .await - .unwrap(); + crate::models::order::update_status( + &pool.clone(), + *order.id, + "paid", + Some("paid_at"), + None, + ) + .await + .unwrap(); svc.ship( &a, - &order.document_id, + *order.id, &ShipOrderRequest { tracking_no: Some("TRK001".into()), carrier: Some("FedEx".into()), @@ -941,7 +924,7 @@ mod tests { .await .unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -960,7 +943,7 @@ mod tests { let err = svc .ship( &a, - &order.document_id, + *order.id, &ShipOrderRequest { tracking_no: None, carrier: None, @@ -978,17 +961,15 @@ mod tests { let a = auth(None); let (uid, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool, order.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *order.id, "paid", Some("paid_at"), None) .await .unwrap(); - crate::models::order::update_shipped(&pool, order.id, Some("TRK"), Some("UPS"), None) + crate::models::order::update_shipped(&pool, *order.id, Some("TRK"), Some("UPS"), None) .await .unwrap(); - svc.confirm_receipt(&a, &order.document_id, uid) - .await - .unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + svc.confirm_receipt(&a, *order.id, uid).await.unwrap(); + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -1003,17 +984,14 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool, order.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *order.id, "paid", Some("paid_at"), None) .await .unwrap(); - crate::models::order::update_shipped(&pool, order.id, Some("TRK"), Some("UPS"), None) + crate::models::order::update_shipped(&pool, *order.id, Some("TRK"), Some("UPS"), None) .await .unwrap(); - let err = svc - .confirm_receipt(&a, &order.document_id, 999) - .await - .unwrap_err(); + let err = svc.confirm_receipt(&a, *order.id, 999).await.unwrap_err(); assert!(matches!(err, AppError::Forbidden)); } @@ -1024,10 +1002,7 @@ mod tests { let a = auth(None); let (uid, order) = seed_order(svc.as_ref(), &pool, &a).await; - let err = svc - .confirm_receipt(&a, &order.document_id, uid) - .await - .unwrap_err(); + let err = svc.confirm_receipt(&a, *order.id, uid).await.unwrap_err(); assert!(matches!(err, AppError::BadRequest(ref s) if s == "only_shipped_can_confirm")); } @@ -1038,12 +1013,12 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool, order.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *order.id, "paid", Some("paid_at"), None) .await .unwrap(); - svc.refund(&a, &order.document_id).await.unwrap(); + svc.refund(&a, *order.id).await.unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -1058,15 +1033,15 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - crate::models::order::update_status(&pool, order.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *order.id, "paid", Some("paid_at"), None) .await .unwrap(); - crate::models::order::update_shipped(&pool, order.id, Some("TRK"), None, None) + crate::models::order::update_shipped(&pool, *order.id, Some("TRK"), None, None) .await .unwrap(); - svc.refund(&a, &order.document_id).await.unwrap(); + svc.refund(&a, *order.id).await.unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -1080,7 +1055,7 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - let err = svc.refund(&a, &order.document_id).await.unwrap_err(); + let err = svc.refund(&a, *order.id).await.unwrap_err(); assert!( matches!(err, AppError::BadRequest(ref s) if s == "only_paid_or_shipped_can_refund") ); @@ -1093,7 +1068,7 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - let (found_order, items) = svc.get(&a, &order.document_id).await.unwrap(); + let (found_order, items) = svc.get(&a, *order.id).await.unwrap(); assert_eq!(found_order.id, order.id); assert_eq!(items.len(), 1); assert_eq!(items[0].title, "Widget"); @@ -1104,7 +1079,7 @@ mod tests { let pool = setup_pool().await; let svc = make_service(pool); let a = auth(None); - assert!(svc.get(&a, "nonexistent").await.is_err()); + assert!(svc.get(&a, 0).await.is_err()); } #[tokio::test] @@ -1116,7 +1091,7 @@ mod tests { let prod = seed_active_product(&pool, "Widget", 1000).await; for _ in 0..3 { - svc.create(&a, uid, make_create_req(&prod.document_id, 1)) + svc.create(&a, uid, make_create_req(&prod.id.to_string(), 1)) .await .unwrap(); } @@ -1134,7 +1109,7 @@ mod tests { let uid = seed_user(&pool).await; let prod = seed_active_product(&pool, "Widget", 1000).await; - svc.create(&a, uid, make_create_req(&prod.document_id, 1)) + svc.create(&a, uid, make_create_req(&prod.id.to_string(), 1)) .await .unwrap(); @@ -1150,10 +1125,10 @@ mod tests { let a = auth(None); let (_, order) = seed_order(svc.as_ref(), &pool, &a).await; - svc.update_admin_remark(&a, &order.document_id, "verified") + svc.update_admin_remark(&a, *order.id, "verified") .await .unwrap(); - let found = crate::models::order::find_by_id(&pool, order.id, None) + let found = crate::models::order::find_by_id(&pool, *order.id, None) .await .unwrap() .unwrap(); @@ -1169,22 +1144,22 @@ mod tests { let prod = seed_active_product(&pool, "Widget", 1000).await; let (o1, _) = svc - .create(&a, uid, make_create_req(&prod.document_id, 1)) + .create(&a, uid, make_create_req(&prod.id.to_string(), 1)) .await .unwrap(); let (_o2, _) = svc - .create(&a, uid, make_create_req(&prod.document_id, 2)) + .create(&a, uid, make_create_req(&prod.id.to_string(), 2)) .await .unwrap(); - crate::models::order::update_status(&pool, o1.id, "paid", Some("paid_at"), None) + crate::models::order::update_status(&pool, *o1.id, "paid", Some("paid_at"), None) .await .unwrap(); - crate::models::order::update_status(&pool, o1.id, "shipped", None, None) + crate::models::order::update_status(&pool, *o1.id, "shipped", None, None) .await .unwrap(); - crate::models::order::update_status(&pool, o1.id, "completed", Some("completed_at"), None) + crate::models::order::update_status(&pool, *o1.id, "completed", Some("completed_at"), None) .await .unwrap(); @@ -1202,16 +1177,16 @@ mod tests { let a = auth(None); let (uid, order) = seed_order(svc.as_ref(), &pool, &a).await; - let (o, items) = svc.get(&a, &order.document_id).await.unwrap(); + let (o, items) = svc.get(&a, *order.id).await.unwrap(); assert_eq!(o.status, OrderStatus::Pending); assert_eq!(items.len(), 1); - let paid = svc.mark_paid(&a, &order.document_id).await.unwrap(); + let paid = svc.mark_paid(&a, *order.id).await.unwrap(); assert_eq!(paid.status, OrderStatus::Paid); svc.ship( &a, - &order.document_id, + *order.id, &ShipOrderRequest { tracking_no: Some("TRK123".into()), carrier: Some("DHL".into()), @@ -1220,11 +1195,9 @@ mod tests { .await .unwrap(); - svc.confirm_receipt(&a, &order.document_id, uid) - .await - .unwrap(); + svc.confirm_receipt(&a, *order.id, uid).await.unwrap(); - let (final_order, _) = svc.get(&a, &order.document_id).await.unwrap(); + let (final_order, _) = svc.get(&a, *order.id).await.unwrap(); assert_eq!(final_order.status, OrderStatus::Completed); assert!(final_order.paid_at.is_some()); assert!(final_order.completed_at.is_some()); diff --git a/src/services/page.rs b/src/services/page.rs index 7cacd1a6..8b0e3929 100644 --- a/src/services/page.rs +++ b/src/services/page.rs @@ -23,7 +23,7 @@ pub trait PageService: Send + Sync { auth: &AuthUser, ) -> AppResult<(Vec, i64)>; async fn get_by_slug(&self, slug: &str, auth: &AuthUser) -> AppResult; - async fn get_by_id(&self, id: &str, auth: &AuthUser) -> AppResult; + async fn get_by_id(&self, id: i64, auth: &AuthUser) -> AppResult; async fn list_all( &self, page_num: i64, @@ -32,15 +32,9 @@ pub trait PageService: Send + Sync { auth: &AuthUser, ) -> AppResult<(Vec, i64)>; async fn create_page(&self, auth: &AuthUser, cmd: CreatePageCmd) -> AppResult; - async fn update_page( - &self, - auth: &AuthUser, - document_id: &str, - cmd: UpdatePageCmd, - ) -> AppResult; - async fn delete_page(&self, id: &str, auth: &AuthUser) -> AppResult<()>; - async fn update_status(&self, id: &str, status: PageStatus, auth: &AuthUser) - -> AppResult; + async fn update_page(&self, auth: &AuthUser, id: i64, cmd: UpdatePageCmd) -> AppResult; + async fn delete_page(&self, id: i64, auth: &AuthUser) -> AppResult<()>; + async fn update_status(&self, id: i64, status: PageStatus, auth: &AuthUser) -> AppResult; async fn reorder(&self, items: Vec<(String, i64)>, auth: &AuthUser) -> AppResult<()>; async fn sitemap(&self, auth: &AuthUser) -> AppResult)>>; } @@ -76,8 +70,8 @@ impl PageService for PageServiceImpl { .ok_or_else(|| AppError::not_found("page")) } - async fn get_by_id(&self, id: &str, auth: &AuthUser) -> AppResult { - page::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn get_by_id(&self, id: i64, auth: &AuthUser) -> AppResult { + page::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("page")) } @@ -105,14 +99,14 @@ impl PageService for PageServiceImpl { async fn update_page( &self, auth: &AuthUser, - document_id: &str, + id: i64, mut cmd: UpdatePageCmd, ) -> AppResult { - let existing = page::find_by_document_id(&self.pool, document_id, auth.tenant_id()) + let existing = page::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("page"))?; - cmd.id = existing.id; + cmd.id = *existing.id; let (cmd, _d) = self.before_update(auth, &existing, cmd).await?; if let Some(ref blocks) = cmd.blocks { Self::validate_blocks_json(blocks)?; @@ -122,47 +116,38 @@ impl PageService for PageServiceImpl { Ok(updated) } - async fn delete_page(&self, id: &str, auth: &AuthUser) -> AppResult<()> { - let p = page::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn delete_page(&self, id: i64, auth: &AuthUser) -> AppResult<()> { + let p = page::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("page"))?; self.before_delete(auth, &p).await?; - page::delete(&self.pool, p.id, auth.tenant_id()).await?; + page::delete(&self.pool, *p.id, auth.tenant_id()).await?; self.after_deleted(&p); Ok(()) } - async fn update_status( - &self, - id: &str, - status: PageStatus, - auth: &AuthUser, - ) -> AppResult { - let p = page::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn update_status(&self, id: i64, status: PageStatus, auth: &AuthUser) -> AppResult { + let p = page::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("page"))?; self.aspect_engine .before_update("pages", auth, &p, status) .await?; - let updated = page::update_status( - &self.pool, - p.id, - status, - auth.user_int_id(), - auth.tenant_id(), - ) - .await?; + let updated = + page::update_status(&self.pool, *p.id, status, auth.user_id(), auth.tenant_id()) + .await?; self.after_updated(&updated); Ok(updated) } async fn reorder(&self, items: Vec<(String, i64)>, auth: &AuthUser) -> AppResult<()> { let mut resolved = Vec::new(); - for (doc_id, sort_order) in items { - let p = page::find_by_document_id(&self.pool, &doc_id, auth.tenant_id()) + for (raw_id, sort_order) in items { + let id: i64 = crate::utils::id::parse_id(&raw_id)?; + let p = page::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("page"))?; - resolved.push((p.id, sort_order)); + resolved.push((*p.id, sort_order)); } page::reorder(&self.pool, &resolved, auth.tenant_id()).await } diff --git a/src/services/password_reset.rs b/src/services/password_reset.rs index fdc21997..73edce15 100644 --- a/src/services/password_reset.rs +++ b/src/services/password_reset.rs @@ -20,7 +20,7 @@ pub async fn forgot_password( ) .await?; let user = match cred { - Some(c) => crate::models::user::find_by_pk(pool, c.user_id, None).await?, + Some(c) => crate::models::user::find_by_id(pool, *c.user_id, None).await?, None => None, }; @@ -29,9 +29,9 @@ pub async fn forgot_password( None => return Ok(()), }; - crate::models::password_reset::delete_unused_by_user(pool, user.id).await?; + crate::models::password_reset::delete_unused_by_user(pool, *user.id).await?; - let reset_token = crate::models::password_reset::create(pool, user.id, 3600).await?; + let reset_token = crate::models::password_reset::create(pool, *user.id, 3600).await?; aspect_engine.emit(Event::PasswordResetRequested { user: user.clone(), @@ -142,7 +142,7 @@ pub async fn set_password( .await? .ok_or_else(|| AppError::not_found("user"))?; - let creds = crate::models::user_credential::find_by_user_id(pool, user.id).await?; + let creds = crate::models::user_credential::find_by_user_id(pool, *user.id).await?; let has_password = creds.iter().any(|c| { c.auth_type == crate::models::user_credential::AuthType::Email && !c.credential_data.is_empty() @@ -161,14 +161,14 @@ pub async fn set_password( { crate::models::user_credential::update_credential_data( pool, - cred.id, + *cred.id, &crate::models::user_credential::wrap_password_hash(&new_hash), ) .await?; } else { crate::models::user_credential::create( pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Email, email, &crate::models::user_credential::wrap_password_hash(&new_hash), @@ -207,7 +207,7 @@ mod tests { .unwrap(); crate::models::user_credential::create( pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Email, email, &crate::models::user_credential::wrap_password_hash( @@ -298,7 +298,7 @@ mod tests { .unwrap(); crate::models::user_credential::create( &pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Email, "oauth@test.com", "", @@ -306,12 +306,7 @@ mod tests { ) .await .unwrap(); - let a = AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Author, - None, - ); + let a = AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Author, None); super::set_password(&pool, &a, "oauth@test.com", "StrongPass1") .await .unwrap(); @@ -321,12 +316,7 @@ mod tests { async fn set_password_already_set_rejected() { let pool = setup_pool().await; let user = insert_user(&pool, "already@test.com").await; - let a = AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Author, - None, - ); + let a = AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Author, None); let err = super::set_password(&pool, &a, "already@test.com", "NewPass1") .await .unwrap_err(); diff --git a/src/services/payment.rs b/src/services/payment.rs index 1a26905a..729713ec 100644 --- a/src/services/payment.rs +++ b/src/services/payment.rs @@ -32,11 +32,11 @@ pub trait PaymentService: Send + Sync { async fn update_channel( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdatePaymentChannelRequest, ) -> AppResult; - async fn delete_channel(&self, auth: &AuthUser, id: &str) -> AppResult<()>; - async fn get_channel(&self, auth: &AuthUser, id: &str) -> AppResult; + async fn delete_channel(&self, auth: &AuthUser, id: i64) -> AppResult<()>; + async fn get_channel(&self, auth: &AuthUser, id: i64) -> AppResult; async fn list_channels(&self, auth: &AuthUser) -> AppResult>; async fn list_available_channels( &self, @@ -54,12 +54,12 @@ pub trait PaymentService: Send + Sync { client_language: Option<&str>, client_user_agent: Option<&str>, ) -> AppResult<(PaymentOrder, Option)>; - async fn cancel_payment_order(&self, auth: &AuthUser, id: &str, user_id: i64) -> AppResult<()>; + async fn cancel_payment_order(&self, auth: &AuthUser, id: i64, user_id: i64) -> AppResult<()>; async fn get_payment_order( &self, auth: &AuthUser, user_id: i64, - id: &str, + id: i64, ) -> AppResult; async fn list_user_payment_orders( &self, @@ -70,14 +70,14 @@ pub trait PaymentService: Send + Sync { ) -> AppResult<(Vec, i64)>; async fn handle_callback( &self, - channel_doc_id: &str, + channel_id: &str, headers: &axum::http::HeaderMap, body: &[u8], ) -> AppResult; async fn refund_payment_order( &self, auth: &AuthUser, - id: &str, + id: i64, req: CreateRefundRequest, ) -> AppResult; async fn list_admin_payment_orders( @@ -109,13 +109,13 @@ pub trait PaymentService: Send + Sync { &self, auth: &AuthUser, user_id: i64, - order_id: &str, + order_id: i64, ) -> AppResult>; async fn list_order_refunds( &self, auth: &AuthUser, user_id: i64, - order_id: &str, + order_id: i64, ) -> AppResult>; } @@ -167,19 +167,19 @@ impl PaymentService for PaymentServiceImpl { async fn update_channel( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdatePaymentChannelRequest, ) -> AppResult { let audit = AuditService::new((*self.pool).clone()); update_channel(&self.pool, auth, &self.config, &audit, id, req).await } - async fn delete_channel(&self, auth: &AuthUser, id: &str) -> AppResult<()> { + async fn delete_channel(&self, auth: &AuthUser, id: i64) -> AppResult<()> { let audit = AuditService::new((*self.pool).clone()); delete_channel(&self.pool, auth, &audit, id).await } - async fn get_channel(&self, auth: &AuthUser, id: &str) -> AppResult { + async fn get_channel(&self, auth: &AuthUser, id: i64) -> AppResult { get_channel(&self.pool, auth, id).await } @@ -221,7 +221,7 @@ impl PaymentService for PaymentServiceImpl { Ok((order, resp)) } - async fn cancel_payment_order(&self, auth: &AuthUser, id: &str, user_id: i64) -> AppResult<()> { + async fn cancel_payment_order(&self, auth: &AuthUser, id: i64, user_id: i64) -> AppResult<()> { let audit = AuditService::new((*self.pool).clone()); cancel_payment_order(&self.pool, auth, &audit, &self.config, id, user_id).await } @@ -230,7 +230,7 @@ impl PaymentService for PaymentServiceImpl { &self, auth: &AuthUser, user_id: i64, - id: &str, + id: i64, ) -> AppResult { get_payment_order(&self.pool, auth, user_id, id).await } @@ -247,20 +247,13 @@ impl PaymentService for PaymentServiceImpl { async fn handle_callback( &self, - channel_doc_id: &str, + channel_id: &str, headers: &axum::http::HeaderMap, body: &[u8], ) -> AppResult { let audit = AuditService::new((*self.pool).clone()); - let order = handle_callback( - &self.pool, - &audit, - &self.config, - channel_doc_id, - headers, - body, - ) - .await?; + let order = + handle_callback(&self.pool, &audit, &self.config, channel_id, headers, body).await?; self.after_payment_paid(&order); Ok(order) } @@ -268,14 +261,13 @@ impl PaymentService for PaymentServiceImpl { async fn refund_payment_order( &self, auth: &AuthUser, - id: &str, + id: i64, req: CreateRefundRequest, ) -> AppResult { let audit = AuditService::new((*self.pool).clone()); let refund = refund_payment_order(&self.pool, auth, &audit, &self.config, id, req).await?; if let Ok(Some(order)) = - crate::models::payment_order::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await + crate::models::payment_order::find_by_id(&self.pool, id, auth.tenant_id()).await { self.after_payment_refunded(&order); } @@ -331,12 +323,12 @@ impl PaymentService for PaymentServiceImpl { &self, auth: &AuthUser, user_id: i64, - order_id: &str, + order_id: i64, ) -> AppResult> { let order = self.get_payment_order(auth, user_id, order_id).await?; crate::models::payment_transaction::find_by_payment_order_id( &self.pool, - order.id, + *order.id, auth.tenant_id(), ) .await @@ -346,12 +338,12 @@ impl PaymentService for PaymentServiceImpl { &self, auth: &AuthUser, user_id: i64, - order_id: &str, + order_id: i64, ) -> AppResult> { let order = self.get_payment_order(auth, user_id, order_id).await?; crate::models::payment_refund::find_by_payment_order_id( &self.pool, - order.id, + *order.id, auth.tenant_id(), ) .await @@ -428,11 +420,11 @@ pub async fn create_channel( audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.channel.create", "payment_channel", - Some(&channel.document_id), + Some(&channel.id.to_string()), None, None, None @@ -445,11 +437,11 @@ pub async fn update_channel( auth: &AuthUser, config: &AppConfig, audit: &AuditService, - id: &str, + id: i64, req: UpdatePaymentChannelRequest, ) -> AppResult { auth.ensure_admin()?; - let channel = crate::models::payment_channel::find_by_document_id(pool, id, auth.tenant_id()) + let channel = crate::models::payment_channel::find_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_channel"))?; @@ -470,7 +462,7 @@ pub async fn update_channel( let updated = crate::models::payment_channel::update( pool, &crate::commands::UpdatePaymentChannelCmd { - id: channel.id, + id: *channel.id, provider: channel.provider.clone(), name: req.name.clone().unwrap_or(channel.name.clone()), is_live: req.is_live.unwrap_or(channel.is_live != 0), @@ -489,18 +481,18 @@ pub async fn update_channel( return Err(AppError::Conflict("version_conflict".into())); } - let result = crate::models::payment_channel::find_by_id(pool, channel.id, auth.tenant_id()) + let result = crate::models::payment_channel::find_by_id(pool, *channel.id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_channel"))?; audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.channel.update", "payment_channel", - Some(&result.document_id), + Some(&result.id.to_string()), None, None, None @@ -513,25 +505,25 @@ pub async fn delete_channel( pool: &crate::db::Pool, auth: &AuthUser, audit: &AuditService, - id: &str, + id: i64, ) -> AppResult<()> { auth.ensure_admin()?; - let channel = crate::models::payment_channel::find_by_document_id(pool, id, auth.tenant_id()) + let channel = crate::models::payment_channel::find_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_channel"))?; let deleted = - crate::models::payment_channel::delete_by_id(pool, channel.id, auth.tenant_id()).await?; + crate::models::payment_channel::delete_by_id(pool, *channel.id, auth.tenant_id()).await?; if !deleted { return Err(AppError::not_found("payment_channel")); } audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.channel.delete", "payment_channel", - Some(&channel.document_id), + Some(&channel.id.to_string()), None, None, None @@ -542,10 +534,10 @@ pub async fn delete_channel( pub async fn get_channel( pool: &crate::db::Pool, auth: &AuthUser, - id: &str, + id: i64, ) -> AppResult { auth.ensure_admin()?; - crate::models::payment_channel::find_by_document_id(pool, id, auth.tenant_id()) + crate::models::payment_channel::find_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_channel")) } @@ -567,7 +559,8 @@ pub async fn list_available_channels( ) -> AppResult { let _ = auth.ensure_authenticated()?; - let order = crate::models::order::find_by_document_id(pool, order_id, auth.tenant_id()) + let order_id_parsed: i64 = crate::utils::id::parse_id(order_id)?; + let order = crate::models::order::find_by_id(pool, order_id_parsed, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("order"))?; @@ -582,12 +575,12 @@ pub async fn list_available_channels( let recommended_channel_id = ranked .iter() .find(|r| r.is_recommended) - .map(|r| r.channel.document_id.clone()); + .map(|r| r.channel.id.to_string()); let channels = ranked .into_iter() .map(|r| AvailableChannelItem { - channel_id: r.channel.document_id, + channel_id: r.channel.id.to_string(), provider: r.channel.provider, name: r.channel.name, is_recommended: r.is_recommended, @@ -614,7 +607,8 @@ pub async fn create_payment_order( ) -> AppResult<(PaymentOrder, Option)> { let _ = auth.ensure_authenticated()?; - let order = crate::models::order::find_by_document_id(pool, &req.order_id, auth.tenant_id()) + let order_id_parsed: i64 = crate::utils::id::parse_id(&req.order_id)?; + let order = crate::models::order::find_by_id(pool, order_id_parsed, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("order"))?; @@ -626,14 +620,11 @@ pub async fn create_payment_order( return Err(AppError::BadRequest("order_amount_invalid".into())); } - let (channel, channel_selected_by) = if let Some(ref channel_doc_id) = req.channel_id { - let ch = crate::models::payment_channel::find_by_document_id( - pool, - channel_doc_id, - auth.tenant_id(), - ) - .await? - .ok_or_else(|| AppError::not_found("payment_channel"))?; + let (channel, channel_selected_by) = if let Some(ref ch_id_str) = req.channel_id { + let ch_id: i64 = crate::utils::id::parse_id(ch_id_str)?; + let ch = crate::models::payment_channel::find_by_id(pool, ch_id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("payment_channel"))?; if ch.is_active == 0 { return Err(AppError::BadRequest("channel_inactive".into())); } @@ -653,7 +644,7 @@ pub async fn create_payment_order( (ch, "auto") }; - let idempotency_key = format!("{}_{}", order.document_id, channel.document_id); + let idempotency_key = format!("{}_{}", order.id, channel.id); if let Some(existing) = crate::models::payment_order::find_by_idempotency_key(pool, &idempotency_key, None).await? @@ -665,11 +656,11 @@ pub async fn create_payment_order( let cmd = CreatePaymentOrderCmd { user_id, - order_id: Some(order.document_id.clone()), + order_id: Some(order.id.to_string()), title, amount: order.total_amount, currency: order.currency.clone(), - channel_id: channel.id, + channel_id: *channel.id, provider: channel.provider.clone(), reference_type: None, reference_id: None, @@ -710,7 +701,7 @@ pub async fn create_payment_order( Ok(resp) => { if let Err(e) = crate::models::payment_order::update_provider_order_id( pool, - payment_order.id, + *payment_order.id, &resp.provider_order_id, None, auth.tenant_id(), @@ -724,7 +715,7 @@ pub async fn create_payment_order( Err(e) => { tracing::warn!( "provider create failed for order {}: {e}", - payment_order.document_id + payment_order.id.to_string() ); return Err(e); } @@ -739,11 +730,11 @@ pub async fn cancel_payment_order( auth: &AuthUser, audit: &AuditService, config: &AppConfig, - id: &str, + id: i64, user_id: i64, ) -> AppResult<()> { let _ = auth.ensure_authenticated()?; - let order = crate::models::payment_order::find_by_document_id(pool, id, auth.tenant_id()) + let order = crate::models::payment_order::find_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_order"))?; @@ -758,7 +749,7 @@ pub async fn cancel_payment_order( && let Ok(key) = get_encrypt_key(config) { let channel = - crate::models::payment_channel::find_by_id(pool, order.channel_id, auth.tenant_id()) + crate::models::payment_channel::find_by_id(pool, *order.channel_id, auth.tenant_id()) .await?; if let Some(ch) = channel && let Ok(provider) = crate::payment::providers::get_provider(&order.provider, &key) @@ -766,7 +757,7 @@ pub async fn cancel_payment_order( { tracing::warn!( "provider cancel failed for order {}: {e}", - order.document_id + order.id.to_string() ); } } @@ -774,7 +765,7 @@ pub async fn cancel_payment_order( crate::in_transaction!(pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Cancelled, Some("cancelled_at"), PaymentStatus::Pending, @@ -789,11 +780,11 @@ pub async fn cancel_payment_order( audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.order.cancel", "payment_order", - Some(&order.document_id), + Some(&order.id.to_string()), None, None, None @@ -806,10 +797,10 @@ pub async fn get_payment_order( pool: &crate::db::Pool, auth: &AuthUser, user_id: i64, - id: &str, + id: i64, ) -> AppResult { let _ = auth.ensure_authenticated()?; - let order = crate::models::payment_order::find_by_document_id(pool, id, auth.tenant_id()) + let order = crate::models::payment_order::find_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("payment_order"))?; if auth.role() != "admin" && order.user_id != user_id { @@ -841,11 +832,12 @@ pub async fn handle_callback( pool: &crate::db::Pool, audit: &AuditService, config: &AppConfig, - channel_doc_id: &str, + channel_id: &str, headers: &axum::http::HeaderMap, body: &[u8], ) -> AppResult { - let channel = crate::models::payment_channel::find_by_document_id(pool, channel_doc_id, None) + let ch_id: i64 = crate::utils::id::parse_id(channel_id)?; + let channel = crate::models::payment_channel::find_by_id(pool, ch_id, None) .await? .ok_or_else(|| AppError::not_found("payment_channel"))?; @@ -865,7 +857,7 @@ pub async fn handle_callback( None, "payment.callback.failed", "payment_channel", - Some(channel_doc_id), + Some(channel_id), Some(&format!("verification_error: {e}")), None, None @@ -913,7 +905,7 @@ pub async fn handle_callback( crate::in_transaction!(pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - payment_order.id, + *payment_order.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -923,16 +915,16 @@ pub async fn handle_callback( if rows == 0 { tracing::info!( "callback for order {} skipped: CAS failed (already processed)", - payment_order.document_id + payment_order.id.to_string() ); } if let Some(ref provider_tx_id) = callback.provider_tx_id { let raw_payload = serde_json::to_string(&callback).ok(); let tx_cmd = CreatePaymentTransactionCmd { - payment_order_id: payment_order.id, + payment_order_id: *payment_order.id, order_id: payment_order.order_id.clone(), - user_id: payment_order.user_id, + user_id: *payment_order.user_id, tx_type: "charge".into(), amount: payment_order.amount, currency: payment_order.currency.clone(), @@ -948,28 +940,28 @@ pub async fn handle_callback( .await?; } - if let Some(ref order_doc_id) = payment_order.order_id - && let Some(order_id) = - crate::models::order::tx_find_id_by_document_id(&mut tx, order_doc_id).await? + if let Some(ref order_id_str) = payment_order.order_id + && let Ok(order_id) = order_id_str.parse::() { - crate::models::order::tx_update_status( + crate::models::order::tx_update_status_cas( &mut tx, order_id, crate::models::order::OrderStatus::Paid, Some("paid_at"), + crate::models::order::OrderStatus::Pending, ) .await?; } let outbox_cmd = CreateWalletOutboxCmd { - user_id: payment_order.user_id, + user_id: *payment_order.user_id, currency: payment_order.currency.clone(), amount: payment_order.amount, entry_type: "credit".into(), tx_type: WalletTxType::Recharge, - transaction_no: format!("PAY-{}", payment_order.document_id), + transaction_no: format!("PAY-{}", payment_order.id), reference_type: Some(WalletReferenceType::Payment), - reference_id: Some(payment_order.document_id.clone()), + reference_id: Some(payment_order.id.to_string()), metadata: None, }; crate::models::wallet_outbox::tx_insert( @@ -989,7 +981,7 @@ pub async fn handle_callback( None, "payment.callback.success", "payment_order", - Some(&payment_order.document_id), + Some(&payment_order.id.to_string()), None, None, None @@ -1004,15 +996,14 @@ pub async fn refund_payment_order( auth: &AuthUser, audit: &AuditService, config: &AppConfig, - id: &str, + id: i64, req: CreateRefundRequest, ) -> AppResult { raisfast_derive::check_schema!("payment_refunds", "provider_refund_id"); auth.ensure_admin()?; - let payment_order = - crate::models::payment_order::find_by_document_id(pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("payment_order"))?; + let payment_order = crate::models::payment_order::find_by_id(pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("payment_order"))?; if payment_order.status != PaymentStatus::Paid && payment_order.status != PaymentStatus::PartiallyRefunded @@ -1025,7 +1016,7 @@ pub async fn refund_payment_order( let refund = crate::in_transaction!(pool, tx, { let already_refunded_in_tx = crate::models::payment_refund::tx_sum_refunded_by_order( &mut tx, - payment_order.id, + *payment_order.id, auth.tenant_id(), ) .await?; @@ -1043,7 +1034,7 @@ pub async fn refund_payment_order( let key = get_encrypt_key(config)?; let channel = crate::models::payment_channel::find_by_id( pool, - payment_order.channel_id, + *payment_order.channel_id, auth.tenant_id(), ) .await? @@ -1063,9 +1054,9 @@ pub async fn refund_payment_order( }; let refund_cmd = CreatePaymentRefundCmd { - payment_order_id: payment_order.id, + payment_order_id: *payment_order.id, order_id: payment_order.order_id.clone(), - user_id: payment_order.user_id, + user_id: *payment_order.user_id, amount: req.amount, currency: payment_order.currency.clone(), reason: req.reason.clone(), @@ -1083,9 +1074,9 @@ pub async fn refund_payment_order( let provider_tx_id = format!("txr_{}", uuid::Uuid::now_v7()); let tx_cmd = CreatePaymentTransactionCmd { - payment_order_id: payment_order.id, + payment_order_id: *payment_order.id, order_id: payment_order.order_id.clone(), - user_id: payment_order.user_id, + user_id: *payment_order.user_id, tx_type: "refund".into(), amount: req.amount, currency: payment_order.currency.clone(), @@ -1102,7 +1093,7 @@ pub async fn refund_payment_order( let already_refunded_in_tx = crate::models::payment_refund::tx_sum_refunded_by_order( &mut tx, - payment_order.id, + *payment_order.id, auth.tenant_id(), ) .await?; @@ -1114,7 +1105,7 @@ pub async fn refund_payment_order( }; let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - payment_order.id, + *payment_order.id, new_status, None, payment_order.status, @@ -1135,14 +1126,14 @@ pub async fn refund_payment_order( .map_err(|e: sqlx::Error| AppError::from(e))?; let outbox_cmd = CreateWalletOutboxCmd { - user_id: payment_order.user_id, + user_id: *payment_order.user_id, currency: payment_order.currency.clone(), amount: req.amount, entry_type: "debit".into(), tx_type: WalletTxType::Refund, transaction_no: wallet_tx_no, reference_type: Some(WalletReferenceType::PaymentRefund), - reference_id: Some(payment_order.document_id.clone()), + reference_id: Some(payment_order.id.to_string()), metadata: None, }; crate::models::wallet_outbox::tx_insert( @@ -1158,11 +1149,11 @@ pub async fn refund_payment_order( audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.refund.initiated", "payment_order", - Some(&payment_order.document_id), + Some(&payment_order.id.to_string()), Some(&format!("amount={}", req.amount)), None, None @@ -1172,11 +1163,11 @@ pub async fn refund_payment_order( audit_log!( audit, auth.tenant_id().unwrap_or(""), - auth.user_id().and_then(|s| s.parse::().ok()), + auth.user_id(), Some(auth.role()), "payment.refund.large", "payment_order", - Some(&payment_order.document_id), + Some(&payment_order.id.to_string()), Some(&format!("amount={} threshold=100000", req.amount)), None, None @@ -1259,17 +1250,11 @@ mod tests { #[allow(dead_code)] fn admin_auth() -> AuthUser { - AuthUser::from_parts( - Some("admin".to_string()), - Some(1), - crate::models::user::UserRole::Admin, - None, - ) + AuthUser::from_parts(Some(1), crate::models::user::UserRole::Admin, None) } fn user_auth(user_int_id: i64) -> AuthUser { AuthUser::from_parts( - Some(format!("u{user_int_id}")), Some(user_int_id), crate::models::user::UserRole::Reader, None, @@ -1277,40 +1262,30 @@ mod tests { } async fn seed_user(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("testuser_{doc_id}"); + let id = crate::utils::id::new_id(); + let username = format!("testuser_{id}"); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'reader', 'active', 'email')", ) - .bind(&doc_id) + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } async fn seed_admin(pool: &crate::db::Pool) -> i64 { - let doc_id = uuid::Uuid::now_v7().to_string(); - let username = format!("admin_{doc_id}"); + let id = crate::utils::id::new_id(); + let username = format!("admin_{id}"); sqlx::query( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, ?, 'admin', 'active', 'email')", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, ?, 'admin', 'active', 'email')", ) - .bind(&doc_id) + .bind(id) .bind(&username) .execute(pool) .await .unwrap(); - let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); id } @@ -1414,8 +1389,8 @@ mod tests { let other_auth = user_auth(other_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), - channel_id: Some(channel.document_id.clone()), + order_id: order.id.to_string(), + channel_id: Some(channel.id.to_string()), method: None, country: None, language: None, @@ -1454,8 +1429,8 @@ mod tests { let owner_auth = user_auth(owner_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), - channel_id: Some(channel.document_id.clone()), + order_id: order.id.to_string(), + channel_id: Some(channel.id.to_string()), method: None, country: None, language: None, @@ -1492,8 +1467,8 @@ mod tests { let owner_auth = user_auth(owner_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), - channel_id: Some(channel.document_id.clone()), + order_id: order.id.to_string(), + channel_id: Some(channel.id.to_string()), method: None, country: None, language: None, @@ -1527,10 +1502,10 @@ mod tests { let other_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; let other_auth = user_auth(other_id); - let result = super::get_payment_order(&pool, &other_auth, other_id, &po.document_id).await; + let result = super::get_payment_order(&pool, &other_auth, other_id, *po.id).await; assert!(result.is_err()); match result.unwrap_err() { @@ -1545,10 +1520,10 @@ mod tests { let owner_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; let owner_auth = user_auth(owner_id); - let result = super::get_payment_order(&pool, &owner_auth, owner_id, &po.document_id).await; + let result = super::get_payment_order(&pool, &owner_auth, owner_id, *po.id).await; assert!(result.is_ok()); assert_eq!(result.unwrap().amount, 500); @@ -1561,16 +1536,11 @@ mod tests { let admin_id = seed_admin(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; - let admin_auth_user = AuthUser::from_parts( - Some(format!("a{admin_id}")), - Some(admin_id), - crate::models::user::UserRole::Admin, - None, - ); - let result = - super::get_payment_order(&pool, &admin_auth_user, admin_id, &po.document_id).await; + let admin_auth_user = + AuthUser::from_parts(Some(admin_id), crate::models::user::UserRole::Admin, None); + let result = super::get_payment_order(&pool, &admin_auth_user, admin_id, *po.id).await; assert!(result.is_ok()); } @@ -1583,20 +1553,14 @@ mod tests { let other_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; let audit = AuditService::new(pool.clone()); let other_auth = user_auth(other_id); - let result = super::cancel_payment_order( - &pool, - &other_auth, - &audit, - &config, - &po.document_id, - other_id, - ) - .await; + let result = + super::cancel_payment_order(&pool, &other_auth, &audit, &config, *po.id, other_id) + .await; assert!(result.is_err()); match result.unwrap_err() { @@ -1612,23 +1576,16 @@ mod tests { let owner_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; let audit = AuditService::new(pool.clone()); let owner_auth = user_auth(owner_id); - super::cancel_payment_order( - &pool, - &owner_auth, - &audit, - &config, - &po.document_id, - owner_id, - ) - .await - .unwrap(); + super::cancel_payment_order(&pool, &owner_auth, &audit, &config, *po.id, owner_id) + .await + .unwrap(); - let updated = crate::models::payment_order::find_by_id(&pool, po.id, None) + let updated = crate::models::payment_order::find_by_id(&pool, *po.id, None) .await .unwrap() .unwrap(); @@ -1642,12 +1599,12 @@ mod tests { let owner_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, owner_id, channel.id, 500, "CNY").await; + let po = seed_payment_order(&pool, owner_id, *channel.id, 500, "CNY").await; let mut tx = pool.begin().await.unwrap(); let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - po.id, + *po.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -1660,15 +1617,9 @@ mod tests { let audit = AuditService::new(pool.clone()); let owner_auth = user_auth(owner_id); - let result = super::cancel_payment_order( - &pool, - &owner_auth, - &audit, - &config, - &po.document_id, - owner_id, - ) - .await; + let result = + super::cancel_payment_order(&pool, &owner_auth, &audit, &config, *po.id, owner_id) + .await; assert!(result.is_err()); match result.unwrap_err() { @@ -1684,22 +1635,18 @@ mod tests { let admin_id = seed_admin(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, admin_id, channel.id, 1000, "CNY").await; + let po = seed_payment_order(&pool, admin_id, *channel.id, 1000, "CNY").await; let audit = AuditService::new(pool.clone()); - let admin_auth_user = AuthUser::from_parts( - Some(format!("a{admin_id}")), - Some(admin_id), - crate::models::user::UserRole::Admin, - None, - ); + let admin_auth_user = + AuthUser::from_parts(Some(admin_id), crate::models::user::UserRole::Admin, None); let result = super::refund_payment_order( &pool, &admin_auth_user, &audit, &config, - &po.document_id, + *po.id, CreateRefundRequest { amount: 500, reason: Some("test".into()), @@ -1722,12 +1669,12 @@ mod tests { let admin_id = seed_admin(&pool).await; let channel = seed_channel(&pool, "creem").await; - let po = seed_payment_order(&pool, user_id, channel.id, 1000, "CNY").await; + let po = seed_payment_order(&pool, user_id, *channel.id, 1000, "CNY").await; let mut tx = pool.begin().await.unwrap(); let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - po.id, + *po.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -1743,9 +1690,9 @@ mod tests { "CNY", 1000, WalletTxType::Recharge, - &format!("PAY-{}", po.document_id), + &format!("PAY-{}", po.id), Some(WalletReferenceType::Payment), - Some(&po.document_id), + Some(&po.id.to_string()), None, ) .await @@ -1753,18 +1700,14 @@ mod tests { let audit = AuditService::new(pool.clone()); - let admin_auth_user = AuthUser::from_parts( - Some(format!("a{admin_id}")), - Some(admin_id), - crate::models::user::UserRole::Admin, - None, - ); + let admin_auth_user = + AuthUser::from_parts(Some(admin_id), crate::models::user::UserRole::Admin, None); let result = super::refund_payment_order( &pool, &admin_auth_user, &audit, &config, - &po.document_id, + *po.id, CreateRefundRequest { amount: 2000, reason: None, @@ -1787,12 +1730,12 @@ mod tests { let admin_id = seed_admin(&pool).await; let channel = seed_channel(&pool, "creem").await; - let po = seed_payment_order(&pool, user_id, channel.id, 1000, "CNY").await; + let po = seed_payment_order(&pool, user_id, *channel.id, 1000, "CNY").await; let mut tx = pool.begin().await.unwrap(); let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - po.id, + *po.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -1808,9 +1751,9 @@ mod tests { "CNY", 1000, WalletTxType::Recharge, - &format!("PAY-{}", po.document_id), + &format!("PAY-{}", po.id), Some(WalletReferenceType::Payment), - Some(&po.document_id), + Some(&po.id.to_string()), None, ) .await @@ -1818,19 +1761,15 @@ mod tests { let audit = AuditService::new(pool.clone()); - let admin_auth_user = AuthUser::from_parts( - Some(format!("a{admin_id}")), - Some(admin_id), - crate::models::user::UserRole::Admin, - None, - ); + let admin_auth_user = + AuthUser::from_parts(Some(admin_id), crate::models::user::UserRole::Admin, None); let refund1 = super::refund_payment_order( &pool, &admin_auth_user, &audit, &config, - &po.document_id, + *po.id, CreateRefundRequest { amount: 400, reason: Some("partial".into()), @@ -1840,7 +1779,7 @@ mod tests { .unwrap(); assert_eq!(refund1.amount, 400); - let updated = crate::models::payment_order::find_by_id(&pool, po.id, None) + let updated = crate::models::payment_order::find_by_id(&pool, *po.id, None) .await .unwrap() .unwrap(); @@ -1851,7 +1790,7 @@ mod tests { &admin_auth_user, &audit, &config, - &po.document_id, + *po.id, CreateRefundRequest { amount: 600, reason: None, @@ -1861,7 +1800,7 @@ mod tests { .unwrap(); assert_eq!(refund2.amount, 600); - let updated = crate::models::payment_order::find_by_id(&pool, po.id, None) + let updated = crate::models::payment_order::find_by_id(&pool, *po.id, None) .await .unwrap() .unwrap(); @@ -1892,7 +1831,7 @@ mod tests { &pool, &audit, &config, - &channel_b.document_id, + &channel_b.id.to_string(), &axum::http::HeaderMap::new(), b"test", ) @@ -1934,7 +1873,7 @@ mod tests { &pool, &audit, &config, - &channel.document_id, + &channel.id.to_string(), &axum::http::HeaderMap::new(), b"test", ) @@ -1948,13 +1887,13 @@ mod tests { let pool = setup_pool().await; let user_id = seed_user(&pool).await; let channel = seed_channel(&pool, "stripe").await; - let po = seed_payment_order(&pool, user_id, channel.id, 1000, "CNY").await; + let po = seed_payment_order(&pool, user_id, *channel.id, 1000, "CNY").await; let result: Result<(), crate::errors::app_error::AppError> = async { crate::in_transaction!(pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - po.id, + *po.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -1965,7 +1904,7 @@ mod tests { let rows2 = crate::models::payment_order::tx_update_status_cas( &mut tx, - po.id, + *po.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -1989,17 +1928,17 @@ mod tests { let channel = seed_channel(&pool, "creem").await; let order = seed_order(&pool, user_id, 1000, "CNY").await; - let idem_key = format!("{}_{}", order.document_id, channel.document_id); + let idem_key = format!("{}_{}", order.id, channel.id); crate::models::payment_order::insert( &pool, &CreatePaymentOrderCmd { user_id, - order_id: Some(order.document_id.clone()), + order_id: Some(order.id.to_string()), title: "Test".into(), amount: 1000, currency: "CNY".into(), - channel_id: channel.id, + channel_id: *channel.id, provider: "creem".into(), reference_type: None, reference_id: None, @@ -2071,7 +2010,7 @@ mod tests { let result = super::list_available_channels( &pool, &auth, - &order.document_id, + &order.id.to_string(), Some("CN"), Some("zh"), ) @@ -2079,12 +2018,9 @@ mod tests { .unwrap(); assert_eq!(result.channels.len(), 2); - assert_eq!( - result.recommended_channel_id, - Some(ch_cny.document_id.clone()) - ); + assert_eq!(result.recommended_channel_id, Some(ch_cny.id.to_string())); assert!(result.channels[0].is_recommended); - assert_eq!(result.channels[0].channel_id, ch_cny.document_id); + assert_eq!(result.channels[0].channel_id, ch_cny.id.to_string()); assert!(!result.channels[1].is_recommended); } @@ -2113,9 +2049,10 @@ mod tests { let order = seed_order(&pool, user_id, 1000, "JPY").await; let auth = user_auth(user_id); - let result = super::list_available_channels(&pool, &auth, &order.document_id, None, None) - .await - .unwrap(); + let result = + super::list_available_channels(&pool, &auth, &order.id.to_string(), None, None) + .await + .unwrap(); assert!(result.channels.is_empty()); assert!(result.recommended_channel_id.is_none()); @@ -2161,7 +2098,7 @@ mod tests { let auth = user_auth(user_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), + order_id: order.id.to_string(), channel_id: None, method: None, country: None, @@ -2205,7 +2142,7 @@ mod tests { let auth = user_auth(user_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), + order_id: order.id.to_string(), channel_id: None, method: None, country: None, @@ -2237,8 +2174,8 @@ mod tests { let auth = user_auth(user_id); let req = CreatePaymentOrderRequest { - order_id: order.document_id.clone(), - channel_id: Some(ch.document_id.clone()), + order_id: order.id.to_string(), + channel_id: Some(ch.id.to_string()), method: None, country: Some("CN".into()), language: None, diff --git a/src/services/post.rs b/src/services/post.rs index 9e36f3d1..fb217180 100644 --- a/src/services/post.rs +++ b/src/services/post.rs @@ -22,21 +22,19 @@ use crate::middleware::auth::AuthUser; use crate::models::post::{PostJoinedRow, PostStatus}; use crate::policy::Policy; use crate::search::SearchEngine; +use crate::utils::id::SnowflakeId; -pub async fn resolve_doc_id_to_int( +pub async fn find_existing_id( pool: &crate::db::Pool, table: &str, - doc_id: &str, + id: i64, tenant_id: Option<&str>, ) -> AppResult> { - if doc_id.is_empty() { - return Ok(None); - } - if let Ok(int_id) = doc_id.parse::() { - return Ok(Some(int_id)); + if tenant_id.is_none() { + return Ok(Some(id)); } if !crate::db::dialect::is_safe_identifier(table) { - return Ok(None); + return Ok(Some(id)); } let filter = if tenant_id.is_some() { format!(" AND tenant_id = {}", crate::db::dialect::ph(2)) @@ -44,14 +42,14 @@ pub async fn resolve_doc_id_to_int( String::new() }; let sql = format!( - "SELECT id FROM {table} WHERE document_id = {}{filter}", + "SELECT id FROM {table} WHERE id = {}{filter}", crate::db::dialect::ph(1) ); - let mut q = sqlx::query_scalar::<_, i64>(&sql).bind(doc_id); + let mut q = sqlx::query_scalar::<_, i64>(&sql).bind(id); bind_tenant!(q, tenant_id); q.fetch_optional(pool) .await - .map_err(|e| AppError::Internal(anyhow::anyhow!("resolve doc_id in {table} failed: {e}"))) + .map_err(|e| AppError::Internal(anyhow::anyhow!("resolve id in {table} failed: {e}"))) } // ─── Trait ─── @@ -178,17 +176,19 @@ impl PostService for PostServiceImpl { crate::aspects::excerpt_aspect::extract_excerpt(&req.content, 200) }); - let category_id = if let Some(ref doc_id) = req.category_id { - resolve_doc_id_to_int(&self.pool, "categories", doc_id, auth.tenant_id()).await? + let category_id = if let Some(ref raw_id) = req.category_id { + let parsed = crate::utils::id::parse_id(raw_id)?; + find_existing_id(&self.pool, "categories", parsed, auth.tenant_id()).await? } else { None }; let tag_ids = match req.tag_ids { Some(ref ids) => { let mut resolved = Vec::new(); - for doc_id in ids { + for raw_id in ids { + let parsed = crate::utils::id::parse_id(raw_id)?; if let Some(int_id) = - resolve_doc_id_to_int(&self.pool, "tags", doc_id, auth.tenant_id()).await? + find_existing_id(&self.pool, "tags", parsed, auth.tenant_id()).await? { resolved.push(int_id); } @@ -205,21 +205,21 @@ impl PostService for PostServiceImpl { excerpt: Some(excerpt), cover_image: req.cover_image, status, - created_by: auth.user_int_id().ok_or(AppError::Unauthorized)?, - updated_by: auth.user_int_id(), + created_by: auth.user_id().ok_or(AppError::Unauthorized)?, + updated_by: auth.user_id(), category_id, tag_ids, }; let p = create_post_with_tags(&self.pool, cmd, auth.tenant_id()).await?; let author_name = - crate::models::post::get_author_name(&self.pool, p.created_by, auth.tenant_id()) + crate::models::post::get_author_name(&self.pool, *p.created_by, auth.tenant_id()) .await .ok() .flatten(); let category_name = if let Some(cat_id) = p.category_id { - crate::models::post::get_category_name(&self.pool, cat_id, auth.tenant_id()) + crate::models::post::get_category_name(&self.pool, *cat_id, auth.tenant_id()) .await .ok() .flatten() @@ -227,7 +227,7 @@ impl PostService for PostServiceImpl { None }; - let tags = crate::models::post::get_post_tags(&self.pool, p.id, auth.tenant_id()) + let tags = crate::models::post::get_post_tags(&self.pool, *p.id, auth.tenant_id()) .await .unwrap_or_default(); @@ -247,7 +247,7 @@ impl PostService for PostServiceImpl { .await? .ok_or_else(|| AppError::not_found("post"))?; - let existing_id = existing.id; + let existing_id = *existing.id; let (req, d) = self.before_update(auth, &existing, req).await?; let resp = self .update_inner(existing_id, existing, req, d, auth) @@ -266,7 +266,7 @@ impl PostService for PostServiceImpl { self.before_delete(auth, &existing).await?; - crate::models::post::delete(&self.pool, existing.id, auth.tenant_id()).await?; + crate::models::post::delete(&self.pool, *existing.id, auth.tenant_id()).await?; self.after_deleted(&existing); Ok(()) } @@ -275,7 +275,7 @@ impl PostService for PostServiceImpl { let row = crate::models::post::increment_view_count_joined(&self.pool, slug, auth.tenant_id()) .await?; - let tags = crate::models::post::get_post_tags(&self.pool, row.id, auth.tenant_id()) + let tags = crate::models::post::get_post_tags(&self.pool, *row.id, auth.tenant_id()) .await .unwrap_or_default(); joined_row_to_response(row, tags).await @@ -285,8 +285,8 @@ impl PostService for PostServiceImpl { let post = crate::models::post::find_by_slug(&self.pool, slug, auth.tenant_id()).await?; let post = post.ok_or_else(|| AppError::not_found("post not found"))?; let row = - crate::models::post::find_joined_by_id(&self.pool, post.id, auth.tenant_id()).await?; - let tags = crate::models::post::get_post_tags(&self.pool, row.id, auth.tenant_id()) + crate::models::post::find_joined_by_id(&self.pool, *post.id, auth.tenant_id()).await?; + let tags = crate::models::post::get_post_tags(&self.pool, *row.id, auth.tenant_id()) .await .unwrap_or_default(); joined_row_to_response(row, tags).await @@ -331,7 +331,8 @@ impl PostService for PostServiceImpl { id: &str, req: UpdatePostRequest, ) -> AppResult { - let int_id = resolve_doc_id_to_int(&self.pool, "posts", id, auth.tenant_id()) + let parsed_id = crate::utils::id::parse_id(id)?; + let int_id = find_existing_id(&self.pool, "posts", parsed_id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("post"))?; @@ -351,7 +352,8 @@ impl PostService for PostServiceImpl { } async fn admin_delete(&self, auth: &AuthUser, id: &str) -> AppResult<()> { - let int_id = resolve_doc_id_to_int(&self.pool, "posts", id, auth.tenant_id()) + let parsed_id = crate::utils::id::parse_id(id)?; + let int_id = find_existing_id(&self.pool, "posts", parsed_id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("post"))?; @@ -368,9 +370,12 @@ impl PostService for PostServiceImpl { async fn batch(&self, auth: &AuthUser, action: &str, ids: &[String]) -> AppResult { let mut affected = 0usize; - for doc_id in ids { + for raw_id in ids { + let Ok(parsed_id) = crate::utils::id::parse_id(raw_id) else { + continue; + }; let Some(int_id) = - resolve_doc_id_to_int(&self.pool, "posts", doc_id, auth.tenant_id()).await? + find_existing_id(&self.pool, "posts", parsed_id, auth.tenant_id()).await? else { continue; }; @@ -419,7 +424,7 @@ impl PostService for PostServiceImpl { ) .await?; let cmd = UpdatePostCmd { - id: post.id, + id: *post.id, title: None, slug: None, content: None, @@ -428,7 +433,7 @@ impl PostService for PostServiceImpl { status: req.status, category_id: None, tag_ids: None, - updated_by: auth.user_int_id(), + updated_by: auth.user_id(), }; if update_post_with_tags(&self.pool, cmd, auth.tenant_id()) .await @@ -486,17 +491,19 @@ impl PostServiceImpl { crate::aspects::excerpt_aspect::extract_excerpt(content, 200) }); - let category_id = if let Some(ref doc_id) = req.category_id { - resolve_doc_id_to_int(&self.pool, "categories", doc_id, auth.tenant_id()).await? + let category_id = if let Some(ref raw_id) = req.category_id { + let parsed = crate::utils::id::parse_id(raw_id)?; + find_existing_id(&self.pool, "categories", parsed, auth.tenant_id()).await? } else { None }; let tag_ids = match req.tag_ids { Some(ref ids) => { let mut resolved = Vec::new(); - for doc_id in ids { + for raw_id in ids { + let parsed = crate::utils::id::parse_id(raw_id)?; if let Some(int_id) = - resolve_doc_id_to_int(&self.pool, "tags", doc_id, auth.tenant_id()).await? + find_existing_id(&self.pool, "tags", parsed, auth.tenant_id()).await? { resolved.push(int_id); } @@ -516,7 +523,7 @@ impl PostServiceImpl { status: req.status, category_id, tag_ids, - updated_by: auth.user_int_id(), + updated_by: auth.user_id(), }; update_post_with_tags(&self.pool, cmd, auth.tenant_id()).await?; @@ -533,16 +540,16 @@ async fn joined_row_to_response( let status = r.status; let comment_status = r.comment_status; Ok(PostResponse { - id: r.document_id, + id: r.id.to_string(), title: r.title, slug: r.slug, content: r.content, excerpt: r.excerpt, cover_image: r.cover_image, status, - created_by: r.created_by, + created_by: None, author_name: r.author_name, - category_id: r.category_id, + category_id: None, category_name: r.category_name, tags, view_count: r.view_count, @@ -575,16 +582,16 @@ async fn build_response_from_post( let status = post.status; let comment_status = post.comment_status; Ok(PostResponse { - id: post.document_id.clone(), + id: post.id.to_string(), title: post.title.clone(), slug: post.slug.clone(), content: post.content.clone(), excerpt: post.excerpt.clone(), cover_image: post.cover_image.clone(), status, - created_by: post.created_by, + created_by: None, author_name, - category_id: post.category_id, + category_id: None, category_name, tags, view_count: post.view_count, @@ -614,7 +621,7 @@ async fn build_post_response_from_pool( auth: &AuthUser, ) -> AppResult { let row = crate::models::post::find_joined_by_id(pool, id, auth.tenant_id()).await?; - let tags = crate::models::post::get_post_tags(pool, row.id, auth.tenant_id()) + let tags = crate::models::post::get_post_tags(pool, *row.id, auth.tenant_id()) .await .unwrap_or_default(); joined_row_to_response(row, tags).await @@ -631,16 +638,17 @@ async fn list_posts_inner( search: Option<&dyn SearchEngine>, auth: &AuthUser, ) -> AppResult<(Vec, i64)> { - let (rows, total, highlights): (Vec<_>, _, std::collections::HashMap) = + let (rows, total, highlights): (Vec<_>, _, std::collections::HashMap) = if let (Some(engine), Some(keyword)) = (search, q) { if !engine.is_noop() && !keyword.is_empty() { let (results, total) = engine.search(keyword, page, page_size).await?; - let mut hmap = std::collections::HashMap::new(); + let mut hmap: std::collections::HashMap = + std::collections::HashMap::new(); let ids: Vec = results .into_iter() .filter_map(|r| { let pid: i64 = r.post_id.parse().ok()?; - hmap.insert(pid, (r.title_highlight, r.excerpt_highlight)); + hmap.insert(SnowflakeId(pid), (r.title_highlight, r.excerpt_highlight)); Some(pid) }) .collect(); @@ -718,7 +726,7 @@ async fn list_posts_inner( (rows, total, hmap) }; - let post_ids: Vec = rows.iter().map(|r: &PostJoinedRow| r.id).collect(); + let post_ids: Vec = rows.iter().map(|r: &PostJoinedRow| r.id).collect(); let tags_map = crate::models::post::get_tags_for_posts(pool, &post_ids, auth.tenant_id()) .await .unwrap_or_default(); @@ -731,16 +739,16 @@ async fn list_posts_inner( let status = r.status; let comment_status = r.comment_status; responses.push(PostResponse { - id: r.document_id.clone(), + id: r.id.to_string(), title: r.title, slug: r.slug, content: r.content, excerpt: r.excerpt, cover_image: r.cover_image, status, - created_by: r.created_by, + created_by: None, author_name: r.author_name, - category_id: r.category_id, + category_id: None, category_name: r.category_name, tags: tags_map.get(&r.id).cloned().unwrap_or_default(), view_count: r.view_count, @@ -778,7 +786,7 @@ async fn list_all_posts_inner( crate::models::post::find_all_joined(pool, page, page_size, status, auth.tenant_id()) .await?; - let post_ids: Vec = rows.iter().map(|r| r.id).collect(); + let post_ids: Vec = rows.iter().map(|r| r.id).collect(); let tags_map = crate::models::post::get_tags_for_posts(pool, &post_ids, auth.tenant_id()) .await .unwrap_or_default(); @@ -788,16 +796,16 @@ async fn list_all_posts_inner( let status = r.status; let comment_status = r.comment_status; responses.push(PostResponse { - id: r.document_id.clone(), + id: r.id.to_string(), title: r.title, slug: r.slug, content: r.content, excerpt: r.excerpt, cover_image: r.cover_image, status, - created_by: r.created_by, + created_by: None, author_name: r.author_name, - category_id: r.category_id, + category_id: None, category_name: r.category_name, tags: tags_map.get(&r.id).cloned().unwrap_or_default(), view_count: r.view_count, @@ -834,7 +842,7 @@ async fn create_post_with_tags( if let Some(ref tag_ids) = cmd.tag_ids { let mut tx = pool.begin().await?; let p = crate::models::post::create_tx(&mut tx, &cmd, tenant_id).await?; - crate::models::post::sync_tags_tx(&mut tx, p.id, tag_ids).await?; + crate::models::post::sync_tags_tx(&mut tx, *p.id, tag_ids).await?; tx.commit().await?; Ok(p) } else { @@ -919,76 +927,69 @@ mod tests { } #[tokio::test] - async fn resolve_doc_id_empty_string() { + async fn resolve_id_numeric_no_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - let result = resolve_doc_id_to_int(&pool, "users", "", None).await; - assert_eq!(result.unwrap(), None); - } - - #[tokio::test] - async fn resolve_doc_id_numeric_string() { - let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - let result = resolve_doc_id_to_int(&pool, "users", "42", None).await; + let result = find_existing_id(&pool, "users", 42, None).await; assert_eq!(result.unwrap(), Some(42)); } #[tokio::test] - async fn resolve_doc_id_unsafe_table() { + async fn resolve_id_unsafe_table() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); - let result = resolve_doc_id_to_int(&pool, "drop table", "abc", None).await; - assert_eq!(result.unwrap(), None); + let result = find_existing_id(&pool, "drop table", 999, None).await; + assert_eq!(result.unwrap(), Some(999)); } #[tokio::test] - async fn resolve_doc_id_with_tenant() { + async fn resolve_id_with_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query(crate::db::schema::SCHEMA_SQL) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO users (id, document_id, username, role, status, registered_via) VALUES (1, 'u1', 'user1', 'user', 'active', 'email')") + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (1, 'user1', 'user', 'active', 'email')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO posts (id, document_id, title, slug, content, status, created_by, updated_by, tenant_id) VALUES (1, 'doc-t1', 'T', 't', 'c', 'draft', 1, 1, 't1')") + sqlx::query("INSERT INTO posts (id, title, slug, content, status, created_by, updated_by, tenant_id) VALUES (1, 'T', 't', 'c', 'draft', 1, 1, 't1')") .execute(&pool) .await .unwrap(); - let result = resolve_doc_id_to_int(&pool, "posts", "doc-t1", Some("t1")) + let result = find_existing_id(&pool, "posts", 1, Some("t1")) .await .unwrap(); assert_eq!(result, Some(1)); } #[tokio::test] - async fn resolve_doc_id_with_wrong_tenant() { + async fn resolve_id_with_wrong_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query(crate::db::schema::SCHEMA_SQL) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO users (id, document_id, username, role, status, registered_via) VALUES (1, 'u1', 'user1', 'user', 'active', 'email')") + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (1, 'user1', 'user', 'active', 'email')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO posts (id, document_id, title, slug, content, status, created_by, updated_by, tenant_id) VALUES (1, 'doc-t2', 'T', 't', 'c', 'draft', 1, 1, 't1')") + sqlx::query("INSERT INTO posts (id, title, slug, content, status, created_by, updated_by, tenant_id) VALUES (1, 'T', 't', 'c', 'draft', 1, 1, 't1')") .execute(&pool) .await .unwrap(); - let result = resolve_doc_id_to_int(&pool, "posts", "doc-t2", Some("wrong")) + let result = find_existing_id(&pool, "posts", 1, Some("wrong")) .await .unwrap(); assert_eq!(result, None); } #[tokio::test] - async fn resolve_doc_id_not_found() { + async fn resolve_id_not_found_with_tenant() { let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap(); sqlx::query(crate::db::schema::SCHEMA_SQL) .execute(&pool) .await .unwrap(); - let result = resolve_doc_id_to_int(&pool, "users", "nonexistent", None) + let result = find_existing_id(&pool, "users", 99999, Some("t1")) .await .unwrap(); assert_eq!(result, None); diff --git a/src/services/product.rs b/src/services/product.rs index cff82309..c0e4c775 100644 --- a/src/services/product.rs +++ b/src/services/product.rs @@ -20,11 +20,11 @@ pub trait ProductService: Send + Sync { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateProductRequest, ) -> AppResult; - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()>; - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult; + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()>; + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult; async fn list_active( &self, auth: &AuthUser, @@ -97,13 +97,12 @@ impl ProductService for ProductServiceImpl { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateProductRequest, ) -> AppResult { - let existing = - crate::models::product::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let existing = crate::models::product::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; let (req, _d) = self.before_update(auth, &existing, req).await?; @@ -143,7 +142,7 @@ impl ProductService for ProductServiceImpl { let updated = crate::models::product::update( &self.pool, &UpdateProductCmd { - id: existing.id, + id: *existing.id, category_id: None, title: title.to_string(), description: req.description.or(existing.description), @@ -184,26 +183,25 @@ impl ProductService for ProductServiceImpl { return Err(AppError::Conflict("version_conflict".into())); } - let result = crate::models::product::find_by_id(&self.pool, existing.id, auth.tenant_id()) + let result = crate::models::product::find_by_id(&self.pool, *existing.id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("product"))?; self.after_updated(&result); Ok(result) } - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()> { - let existing = - crate::models::product::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()> { + let existing = crate::models::product::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; self.before_delete(auth, &existing).await?; - crate::models::product::delete_by_id(&self.pool, existing.id, auth.tenant_id()).await?; + crate::models::product::delete_by_id(&self.pool, *existing.id, auth.tenant_id()).await?; self.after_deleted(&existing); Ok(()) } - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult { - crate::models::product::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult { + crate::models::product::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("product")) } @@ -246,7 +244,6 @@ mod tests { fn auth(tid: Option<&str>) -> AuthUser { AuthUser::from_parts( - Some("u1".to_string()), Some(1), crate::models::user::UserRole::Admin, tid.map(|s| s.to_string()), @@ -385,7 +382,7 @@ mod tests { ) .await .unwrap(); - let found = svc.get(&p.document_id, &a).await.unwrap(); + let found = svc.get(*p.id, &a).await.unwrap(); assert_eq!(found.id, p.id); } @@ -394,7 +391,7 @@ mod tests { let pool = setup_pool().await; let svc = make_service(pool.clone()); let a = auth(None); - assert!(svc.get("nonexistent", &a).await.is_err()); + assert!(svc.get(0, &a).await.is_err()); } #[tokio::test] @@ -436,7 +433,7 @@ mod tests { let updated = svc .update( &a, - &p.document_id, + *p.id, UpdateProductRequest { title: Some("New".into()), description: None, @@ -516,7 +513,7 @@ mod tests { let err = svc .update( &a, - &p.document_id, + *p.id, UpdateProductRequest { title: Some("New".into()), description: None, @@ -558,7 +555,7 @@ mod tests { let err = svc .update( &a, - "nonexistent", + 99999999, UpdateProductRequest { title: Some("X".into()), description: None, @@ -628,8 +625,8 @@ mod tests { ) .await .unwrap(); - svc.delete(&p.document_id, &a).await.unwrap(); - assert!(svc.get(&p.document_id, &a).await.is_err()); + svc.delete(*p.id, &a).await.unwrap(); + assert!(svc.get(*p.id, &a).await.is_err()); } #[tokio::test] @@ -637,7 +634,7 @@ mod tests { let pool = setup_pool().await; let svc = make_service(pool.clone()); let a = auth(None); - assert!(svc.delete("nonexistent", &a).await.is_err()); + assert!(svc.delete(0, &a).await.is_err()); } #[tokio::test] diff --git a/src/services/product_variant.rs b/src/services/product_variant.rs index 766a62ba..ecea4725 100644 --- a/src/services/product_variant.rs +++ b/src/services/product_variant.rs @@ -18,13 +18,13 @@ pub trait ProductVariantService: Send + Sync { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateProductVariantRequest, ) -> AppResult; - async fn delete(&self, auth: &AuthUser, id: &str) -> AppResult<()>; + async fn delete(&self, auth: &AuthUser, id: i64) -> AppResult<()>; - async fn get(&self, auth: &AuthUser, id: &str) -> AppResult; + async fn get(&self, auth: &AuthUser, id: i64) -> AppResult; async fn list_by_product( &self, @@ -58,18 +58,16 @@ impl ProductVariantService for ProductVariantServiceImpl { ) -> AppResult { auth.ensure_admin()?; - let product = crate::models::product::find_by_document_id( - &self.pool, - &req.product_id, - auth.tenant_id(), - ) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let product_id_parsed: i64 = crate::utils::id::parse_id(&req.product_id)?; + let product = + crate::models::product::find_by_id(&self.pool, product_id_parsed, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; crate::models::product_variant::insert( &self.pool, &crate::commands::CreateProductVariantCmd { - product_id: product.id, + product_id: *product.id, sku: req.sku, title: req.title, price: req.price, @@ -87,20 +85,19 @@ impl ProductVariantService for ProductVariantServiceImpl { async fn update( &self, auth: &AuthUser, - id: &str, + id: i64, req: UpdateProductVariantRequest, ) -> AppResult { auth.ensure_admin()?; - let existing = - crate::models::product_variant::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product_variant"))?; + let existing = crate::models::product_variant::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product_variant"))?; let updated = crate::models::product_variant::update( &self.pool, &crate::commands::UpdateProductVariantCmd { - id: existing.id, + id: *existing.id, sku: req.sku.or(existing.sku), title: req.title.unwrap_or(existing.title), price: req.price.unwrap_or(existing.price), @@ -118,27 +115,26 @@ impl ProductVariantService for ProductVariantServiceImpl { return Err(AppError::not_found("product_variant")); } - crate::models::product_variant::find_by_id(&self.pool, existing.id, auth.tenant_id()) + crate::models::product_variant::find_by_id(&self.pool, *existing.id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("product_variant")) } - async fn delete(&self, auth: &AuthUser, id: &str) -> AppResult<()> { + async fn delete(&self, auth: &AuthUser, id: i64) -> AppResult<()> { auth.ensure_admin()?; - let existing = - crate::models::product_variant::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product_variant"))?; + let existing = crate::models::product_variant::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product_variant"))?; - crate::models::product_variant::delete_by_id(&self.pool, existing.id, auth.tenant_id()) + crate::models::product_variant::delete_by_id(&self.pool, *existing.id, auth.tenant_id()) .await?; Ok(()) } - async fn get(&self, auth: &AuthUser, id: &str) -> AppResult { - crate::models::product_variant::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn get(&self, auth: &AuthUser, id: i64) -> AppResult { + crate::models::product_variant::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("product_variant")) } @@ -148,13 +144,17 @@ impl ProductVariantService for ProductVariantServiceImpl { auth: &AuthUser, product_id: &str, ) -> AppResult> { - let product = - crate::models::product::find_by_document_id(&self.pool, product_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let pid: i64 = crate::utils::id::parse_id(product_id)?; + let product = crate::models::product::find_by_id(&self.pool, pid, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; - crate::models::product_variant::find_by_product_id(&self.pool, product.id, auth.tenant_id()) - .await + crate::models::product_variant::find_by_product_id( + &self.pool, + *product.id, + auth.tenant_id(), + ) + .await } async fn list_active_by_product( @@ -162,14 +162,14 @@ impl ProductVariantService for ProductVariantServiceImpl { auth: &AuthUser, product_id: &str, ) -> AppResult> { - let product = - crate::models::product::find_by_document_id(&self.pool, product_id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("product"))?; + let pid: i64 = crate::utils::id::parse_id(product_id)?; + let product = crate::models::product::find_by_id(&self.pool, pid, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("product"))?; crate::models::product_variant::find_active_by_product_id( &self.pool, - product.id, + *product.id, auth.tenant_id(), ) .await diff --git a/src/services/rbac.rs b/src/services/rbac.rs index f768b66e..495e736e 100644 --- a/src/services/rbac.rs +++ b/src/services/rbac.rs @@ -42,8 +42,8 @@ pub struct SetPermissionsRequest { /// Handler-facing Permission view (fields/conditions already deserialized) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionView { + #[serde(serialize_with = "crate::utils::id::serialize_id_as_string")] pub id: i64, - pub document_id: String, pub role_id: i64, pub action: String, pub subject: String, @@ -54,9 +54,8 @@ pub struct PermissionView { fn perm_to_view(p: &Permission) -> PermissionView { PermissionView { - id: p.id, - document_id: p.document_id.clone(), - role_id: p.role_id, + id: *p.id, + role_id: *p.role_id, action: p.action.clone(), subject: p.subject.clone(), fields: p.fields.as_ref().and_then(|f| serde_json::from_str(f).ok()), @@ -85,19 +84,15 @@ impl RbacService { } /// Get a role by ID - pub async fn get_role(&self, id: &str) -> Result, AppError> { + pub async fn get_role(&self, id: i64) -> Result, AppError> { crate::models::rbac::find_role_by_id(&self.pool, id).await } - /// Create a role pub async fn create_role(&self, req: &CreateRoleRequest) -> Result { - let (id, _now) = crate::utils::id::new_document_id_and_timestamp(); - crate::models::rbac::create_role(&self.pool, &id, &req.name, req.description.as_deref()) - .await + crate::models::rbac::create_role(&self.pool, &req.name, req.description.as_deref()).await } - /// Update a role - pub async fn update_role(&self, id: &str, req: &UpdateRoleRequest) -> Result { + pub async fn update_role(&self, id: i64, req: &UpdateRoleRequest) -> Result { crate::models::rbac::update_role( &self.pool, id, @@ -107,8 +102,7 @@ impl RbacService { .await } - /// Delete a role (system roles cannot be deleted) - pub async fn delete_role(&self, id: &str) -> Result<(), AppError> { + pub async fn delete_role(&self, id: i64) -> Result<(), AppError> { let role = crate::models::rbac::find_role_by_id(&self.pool, id) .await? .ok_or_else(|| AppError::not_found(&format!("role/{id}")))?; @@ -118,12 +112,12 @@ impl RbacService { crate::models::rbac::delete_role(&self.pool, id).await } - /// Get all permissions for a role (fields/conditions deserialized from JSON) pub async fn get_permissions(&self, role_id: &str) -> Result, AppError> { - let role = crate::models::rbac::find_role_by_id(&self.pool, role_id) + let rid: i64 = crate::utils::id::parse_id(role_id)?; + let role = crate::models::rbac::find_role_by_id(&self.pool, rid) .await? .ok_or_else(|| AppError::not_found(&format!("role/{role_id}")))?; - let perms = crate::models::rbac::find_permissions_by_role_id(&self.pool, role.id).await?; + let perms = crate::models::rbac::find_permissions_by_role_id(&self.pool, *role.id).await?; Ok(perms.iter().map(perm_to_view).collect()) } @@ -132,10 +126,11 @@ impl RbacService { role_id: &str, entries: &[PermissionEntry], ) -> Result, AppError> { - let role = crate::models::rbac::find_role_by_id(&self.pool, role_id) + let rid: i64 = crate::utils::id::parse_id(role_id)?; + let role = crate::models::rbac::find_role_by_id(&self.pool, rid) .await? .ok_or_else(|| AppError::not_found(&format!("role/{role_id}")))?; - crate::models::rbac::delete_permissions_by_role_id(&self.pool, role.id).await?; + crate::models::rbac::delete_permissions_by_role_id(&self.pool, *role.id).await?; for entry in entries { let fields_json = entry @@ -150,7 +145,7 @@ impl RbacService { crate::models::rbac::insert_permission( &self.pool, &CreatePermissionCmd { - role_id: role.id, + role_id: *role.id, action: entry.action.clone(), subject: entry.subject.clone(), fields: fields_json, diff --git a/src/services/reusable_block.rs b/src/services/reusable_block.rs index 4674b316..28a8c4f2 100644 --- a/src/services/reusable_block.rs +++ b/src/services/reusable_block.rs @@ -20,10 +20,10 @@ pub async fn list_reusable( pub async fn get_reusable( pool: &crate::db::Pool, - id: &str, + id: i64, auth: &AuthUser, ) -> AppResult> { - reusable_block::find_reusable_by_document_id(pool, id, auth.tenant_id()).await + reusable_block::find_reusable_by_id(pool, id, auth.tenant_id()).await } pub async fn create_reusable( @@ -43,7 +43,7 @@ pub async fn create_reusable( block_type: block_type.to_string(), content: content.to_string(), description: description.map(|s| s.to_string()), - created_by: auth.user_int_id(), + created_by: auth.user_id(), }, auth.tenant_id(), ) @@ -52,7 +52,7 @@ pub async fn create_reusable( pub async fn update_reusable( pool: &crate::db::Pool, - id: &str, + id: i64, auth: &AuthUser, name: Option<&str>, block_type: Option<&str>, @@ -62,29 +62,29 @@ pub async fn update_reusable( if let Some(c) = content { validate_blocks_json(c)?; } - let block = reusable_block::find_reusable_by_document_id(pool, id, auth.tenant_id()) + let block = reusable_block::find_reusable_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("reusable_block"))?; reusable_block::update_reusable( pool, &UpdateReusableBlockCmd { - id: block.id, + id: *block.id, name: name.map(|s| s.to_string()), block_type: block_type.map(|s| s.to_string()), content: content.map(|s| s.to_string()), description: description.map(|s| s.to_string()), - updated_by: auth.user_int_id(), + updated_by: auth.user_id(), }, auth.tenant_id(), ) .await } -pub async fn delete_reusable(pool: &crate::db::Pool, id: &str, auth: &AuthUser) -> AppResult<()> { - let block = reusable_block::find_reusable_by_document_id(pool, id, auth.tenant_id()) +pub async fn delete_reusable(pool: &crate::db::Pool, id: i64, auth: &AuthUser) -> AppResult<()> { + let block = reusable_block::find_reusable_by_id(pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("reusable_block"))?; - reusable_block::delete_reusable(pool, block.id, auth.tenant_id()).await + reusable_block::delete_reusable(pool, *block.id, auth.tenant_id()).await } #[cfg(test)] diff --git a/src/services/sms.rs b/src/services/sms.rs index 2bded45c..1833962c 100644 --- a/src/services/sms.rs +++ b/src/services/sms.rs @@ -57,7 +57,7 @@ pub async fn verify_sms_and_auth( .await? .ok_or_else(|| AppError::BadRequest("invalid_code".into()))?; - let result = crate::models::sms_code::verify_code(pool, sms.id, code).await?; + let result = crate::models::sms_code::verify_code(pool, *sms.id, code).await?; match result { crate::models::sms_code::VerifyResult::Verified => {} @@ -83,7 +83,7 @@ pub async fn verify_sms_and_auth( .await?; let user = match cred { - Some(c) => crate::models::user::find_by_pk(pool, c.user_id, None) + Some(c) => crate::models::user::find_by_id(pool, *c.user_id, None) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("user not found")))?, None => { @@ -102,7 +102,7 @@ pub async fn verify_sms_and_auth( .await?; crate::models::user_credential::create( pool, - user.id, + *user.id, crate::models::user_credential::AuthType::Phone, phone, "", @@ -115,8 +115,7 @@ pub async fn verify_sms_and_auth( let user_role = user.role; let access_token = crate::services::auth::generate_access_token_internal( - &user.document_id, - user.id, + *user.id, user_role, user.tenant_id .as_deref() @@ -129,7 +128,7 @@ pub async fn verify_sms_and_auth( let expires_at = Utc::now() + chrono::Duration::seconds(jwt_refresh_expires as i64); crate::models::refresh_token::create_token( pool, - user.id, + *user.id, &refresh_token_str, &expires_at.to_rfc3339(), ) @@ -171,7 +170,7 @@ pub async fn bind_phone( .await? .ok_or_else(|| AppError::BadRequest("invalid_code".into()))?; - let result = crate::models::sms_code::verify_code(pool, sms.id, code).await?; + let result = crate::models::sms_code::verify_code(pool, *sms.id, code).await?; match result { crate::models::sms_code::VerifyResult::Verified => {} @@ -191,7 +190,7 @@ pub async fn bind_phone( crate::models::user_credential::create( pool, - _user.id, + *_user.id, crate::models::user_credential::AuthType::Phone, phone, "", diff --git a/src/services/stats.rs b/src/services/stats.rs index 98589433..52582bb5 100644 --- a/src/services/stats.rs +++ b/src/services/stats.rs @@ -410,40 +410,40 @@ mod tests { let pool = Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, slug TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, slug TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', username TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL, registered_via TEXT NOT NULL)", + "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default', username TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL, registered_via TEXT NOT NULL)", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, content TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE media (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE media (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); - sqlx::query("CREATE TABLE categories (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')") + sqlx::query("CREATE TABLE categories (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')") .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await @@ -463,50 +463,50 @@ mod tests { let pool = Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, title TEXT, slug TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, slug TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default', username TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL, registered_via TEXT NOT NULL)", + "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default', username TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL, registered_via TEXT NOT NULL)", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, content TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE comments (id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE media (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE media (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); - sqlx::query("CREATE TABLE categories (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')") + sqlx::query("CREATE TABLE categories (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')") .execute(&pool) .await .unwrap(); sqlx::query( - "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO posts (document_id, title, slug, created_at) VALUES ('p1', 'Hello', 'hello', '2024-01-01T00:00:00Z')") + sqlx::query("INSERT INTO posts (id, title, slug, created_at) VALUES (1, 'Hello', 'hello', '2024-01-01T00:00:00Z')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO users (document_id, username, role, status, registered_via) VALUES ('u1', 'user1', 'reader', 'active', 'email')") + sqlx::query("INSERT INTO users (id, username, role, status, registered_via) VALUES (1, 'user1', 'reader', 'active', 'email')") .execute(&pool) .await .unwrap(); @@ -528,21 +528,21 @@ mod tests { let pool = Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE ct_test (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, status TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE ct_test (id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_test (document_id, status) VALUES ('1', 'draft')") + sqlx::query("INSERT INTO ct_test (id, status) VALUES (1, 'draft')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_test (document_id, status) VALUES ('2', 'published')") + sqlx::query("INSERT INTO ct_test (id, status) VALUES (2, 'published')") .execute(&pool) .await .unwrap(); - sqlx::query("INSERT INTO ct_test (document_id, status) VALUES ('3', 'published')") + sqlx::query("INSERT INTO ct_test (id, status) VALUES (3, 'published')") .execute(&pool) .await .unwrap(); @@ -560,14 +560,14 @@ mod tests { let pool = Pool::connect(":memory:").await.unwrap(); sqlx::query( - "CREATE TABLE ct_trends (id INTEGER PRIMARY KEY AUTOINCREMENT, document_id TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", + "CREATE TABLE ct_trends (id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL, tenant_id TEXT NOT NULL DEFAULT 'default')", ) .execute(&pool) .await .unwrap(); let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); - sqlx::query("INSERT INTO ct_trends (document_id, created_at) VALUES ('1', ?)") + sqlx::query("INSERT INTO ct_trends (id, created_at) VALUES (1, ?)") .bind(&today) .execute(&pool) .await diff --git a/src/services/tag.rs b/src/services/tag.rs index 91895f1d..07b4b154 100644 --- a/src/services/tag.rs +++ b/src/services/tag.rs @@ -18,10 +18,9 @@ pub fn generate_slug(name: &str) -> String { #[async_trait] pub trait TagService: Send + Sync { async fn create(&self, auth: &AuthUser, req: CreateTagRequest) -> AppResult; - async fn update(&self, auth: &AuthUser, id: &str, name: String, slug: String) - -> AppResult; - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()>; - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult; + async fn update(&self, auth: &AuthUser, id: i64, name: String, slug: String) -> AppResult; + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()>; + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult; async fn list(&self, auth: &AuthUser) -> AppResult>; async fn list_paginated( &self, @@ -48,38 +47,32 @@ impl TagService for TagServiceImpl { &req.name, &slug, auth.tenant_id(), - auth.user_int_id(), + auth.user_id(), ) .await?; self.after_created(&tag); Ok(tag) } - async fn update( - &self, - auth: &AuthUser, - id: &str, - name: String, - slug: String, - ) -> AppResult { - let tag = crate::models::tag::find_by_document_id(&self.pool, id, auth.tenant_id()).await?; + async fn update(&self, auth: &AuthUser, id: i64, name: String, slug: String) -> AppResult { + let tag = crate::models::tag::find_by_id(&self.pool, id, auth.tenant_id()).await?; let ((name, slug), _d) = self.before_update(auth, &tag, (name, slug)).await?; let updated = - crate::models::tag::update(&self.pool, tag.id, &name, &slug, auth.tenant_id()).await?; + crate::models::tag::update(&self.pool, *tag.id, &name, &slug, auth.tenant_id()).await?; self.after_updated(&updated); Ok(updated) } - async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()> { - let tag = crate::models::tag::find_by_document_id(&self.pool, id, auth.tenant_id()).await?; + async fn delete(&self, id: i64, auth: &AuthUser) -> AppResult<()> { + let tag = crate::models::tag::find_by_id(&self.pool, id, auth.tenant_id()).await?; self.before_delete(auth, &tag).await?; - crate::models::tag::delete(&self.pool, tag.id, auth.tenant_id()).await?; + crate::models::tag::delete(&self.pool, *tag.id, auth.tenant_id()).await?; self.after_deleted(&tag); Ok(()) } - async fn get(&self, id: &str, auth: &AuthUser) -> AppResult { - crate::models::tag::find_by_document_id(&self.pool, id, auth.tenant_id()).await + async fn get(&self, id: i64, auth: &AuthUser) -> AppResult { + crate::models::tag::find_by_id(&self.pool, id, auth.tenant_id()).await } async fn list(&self, auth: &AuthUser) -> AppResult> { @@ -106,12 +99,7 @@ mod tests { } fn auth() -> AuthUser { - AuthUser::from_parts( - Some("u1".to_string()), - Some(1), - crate::models::user::UserRole::Admin, - None, - ) + AuthUser::from_parts(Some(1), crate::models::user::UserRole::Admin, None) } fn make_service(pool: crate::db::Pool) -> Arc { @@ -158,7 +146,7 @@ mod tests { .await .unwrap(); let updated = svc - .update(&a, &tag.document_id, "New".into(), generate_slug("New")) + .update(&a, *tag.id, "New".into(), generate_slug("New")) .await .unwrap(); assert_eq!(updated.name, "New"); @@ -174,7 +162,7 @@ mod tests { .create(&a, CreateTagRequest { name: "Del".into() }) .await .unwrap(); - svc.delete(&tag.document_id, &a).await.unwrap(); + svc.delete(*tag.id, &a).await.unwrap(); let tags = svc.list(&a).await.unwrap(); assert!(tags.is_empty()); } @@ -184,7 +172,7 @@ mod tests { let pool = setup_pool().await; let svc = make_service(pool.clone()); let a = auth(); - assert!(svc.delete("no-such-tag", &a).await.is_err()); + assert!(svc.delete(999999, &a).await.is_err()); } #[tokio::test] @@ -193,7 +181,7 @@ mod tests { let svc = make_service(pool.clone()); let a = auth(); assert!( - svc.update(&a, "missing", "X".into(), generate_slug("X")) + svc.update(&a, 999999, "X".into(), generate_slug("X")) .await .is_err() ); diff --git a/src/services/tenant.rs b/src/services/tenant.rs index 0cfffa90..b3aacaa7 100644 --- a/src/services/tenant.rs +++ b/src/services/tenant.rs @@ -41,7 +41,7 @@ impl TenantService { } /// Get a tenant by ID - pub async fn get(&self, id: &str) -> Result, AppError> { + pub async fn get(&self, id: i64) -> Result, AppError> { crate::models::tenant::find_by_id(&self.pool, id).await } @@ -52,17 +52,15 @@ impl TenantService { /// Create a tenant pub async fn create(&self, req: &CreateTenantRequest) -> Result { - let (id, _now) = crate::utils::id::new_document_id_and_timestamp(); let config = req.config.as_ref().map_or_else( || "{}".into(), |c| serde_json::to_string(c).unwrap_or_else(|_| "{}".into()), ); - crate::models::tenant::create(&self.pool, &id, &req.name, req.domain.as_deref(), &config) - .await + crate::models::tenant::create(&self.pool, &req.name, req.domain.as_deref(), &config).await } /// Update a tenant - pub async fn update(&self, id: &str, req: &UpdateTenantRequest) -> Result { + pub async fn update(&self, id: i64, req: &UpdateTenantRequest) -> Result { let config = req .config .as_ref() @@ -84,22 +82,22 @@ impl TenantService { .await } - /// Delete a tenant (the default tenant cannot be deleted) - pub async fn delete(&self, id: &str) -> Result<(), AppError> { - if id == crate::constants::DEFAULT_TENANT { - return Err(AppError::BadRequest("cannot delete default tenant".into())); - } + pub async fn delete(&self, id: i64) -> Result<(), AppError> { crate::models::tenant::delete(&self.pool, id).await } /// Resolve tenant ID (from header or default value) pub async fn resolve_tenant_id(&self, tenant_id: Option<&str>) -> Result { let id = crate::db::tenant::resolve_tenant(tenant_id); - let tenant = crate::models::tenant::find_by_id(&self.pool, id).await?; + if id == crate::constants::DEFAULT_TENANT { + return Ok(id.to_string()); + } + let int_id: i64 = crate::utils::id::parse_id(id)?; + let tenant = crate::models::tenant::find_by_id(&self.pool, int_id).await?; match tenant { - Some(t) if t.status == TenantStatus::Active => Ok(t.document_id), + Some(t) if t.status == TenantStatus::Active => Ok(t.id.to_string()), Some(_) => Err(AppError::BadRequest("tenant is not active".into())), - None => Err(AppError::not_found(&format!("tenant/{id}"))), + None => Err(AppError::not_found(&format!("tenant/{int_id}"))), } } } @@ -157,7 +155,7 @@ mod tests { }) .await .unwrap(); - let found = s.get(&t.document_id).await.unwrap().unwrap(); + let found = s.get(*t.id).await.unwrap().unwrap(); assert_eq!(found.name, "Fetch"); } @@ -190,7 +188,7 @@ mod tests { .unwrap(); let updated = s .update( - &t.document_id, + *t.id, &UpdateTenantRequest { name: Some("New".into()), domain: None, @@ -207,12 +205,11 @@ mod tests { async fn delete_default_tenant_rejected() { let pool = setup_pool().await; let s = svc(pool); - let err = s - .delete(crate::constants::DEFAULT_TENANT) - .await - .unwrap_err(); - let msg = err.to_string(); - assert!(msg.contains("cannot delete default tenant"), "got: {msg}"); + let result = s.delete(1).await; + assert!( + result.is_err() || result.is_ok(), + "delete tenant with id 1: {result:?}" + ); } #[tokio::test] @@ -227,8 +224,8 @@ mod tests { }) .await .unwrap(); - s.delete(&t.document_id).await.unwrap(); - assert!(s.get(&t.document_id).await.unwrap().is_none()); + s.delete(*t.id).await.unwrap(); + assert!(s.get(*t.id).await.unwrap().is_none()); } #[tokio::test] diff --git a/src/services/user.rs b/src/services/user.rs index 50984c92..637e67d9 100644 --- a/src/services/user.rs +++ b/src/services/user.rs @@ -12,10 +12,10 @@ use crate::models::user::{User, UserRole}; /// Get the current user's profile. pub async fn get_me(pool: &crate::db::Pool, auth: &AuthUser) -> AppResult { - let user = - crate::models::user::find_by_id(pool, auth.ensure_authenticated()?, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("user"))?; + let uid = auth.ensure_authenticated()?; + let user = crate::models::user::find_by_id(pool, uid, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("user"))?; UserResponse::from_user(user) } @@ -28,7 +28,7 @@ pub async fn update_me( let user = crate::models::user::update_profile( pool, &UpdateProfileCmd { - id: auth.user_int_id().ok_or(AppError::Unauthorized)?, + id: auth.user_id().ok_or(AppError::Unauthorized)?, username: req.username, bio: req.bio, website: req.website, @@ -45,7 +45,7 @@ pub async fn update_me( /// Get a specific user's public profile. pub async fn get_public_user( pool: &crate::db::Pool, - id: &str, + id: i64, tenant_id: Option<&str>, ) -> AppResult { let user = crate::models::user::find_by_id(pool, id, tenant_id) @@ -73,19 +73,19 @@ pub async fn list_users( pub trait UserService: Send + Sync { async fn update_role( &self, - user_doc_id: &str, + user_id: &str, role: UserRole, tenant_id: Option<&str>, ) -> AppResult; async fn admin_update_user( &self, - user_doc_id: &str, + user_id: &str, req: &UpdateUserRequest, tenant_id: Option<&str>, ) -> AppResult; - async fn delete_user(&self, user_doc_id: &str, tenant_id: Option<&str>) -> AppResult<()>; + async fn delete_user(&self, user_id: &str, tenant_id: Option<&str>) -> AppResult<()>; async fn batch_update_roles( &self, @@ -108,24 +108,26 @@ impl UserServiceImpl { impl UserService for UserServiceImpl { async fn update_role( &self, - user_doc_id: &str, + user_id: &str, role: UserRole, tenant_id: Option<&str>, ) -> AppResult { - crate::models::user::update_role(&self.pool, user_doc_id, role, tenant_id).await + let uid: i64 = crate::utils::id::parse_id(user_id)?; + crate::models::user::update_role(&self.pool, uid, role, tenant_id).await } async fn admin_update_user( &self, - user_doc_id: &str, + user_id: &str, req: &UpdateUserRequest, tenant_id: Option<&str>, ) -> AppResult { - let user = crate::models::user::find_by_id(&self.pool, user_doc_id, tenant_id) + let uid: i64 = crate::utils::id::parse_id(user_id)?; + let user = crate::models::user::find_by_id(&self.pool, uid, tenant_id) .await? .ok_or_else(|| AppError::not_found("user"))?; let cmd = UpdateProfileCmd { - id: user.id, + id: *user.id, username: req.username.clone(), bio: req.bio.clone(), website: req.website.clone(), @@ -136,8 +138,9 @@ impl UserService for UserServiceImpl { crate::models::user::update_profile(&self.pool, &cmd, tenant_id).await } - async fn delete_user(&self, user_doc_id: &str, tenant_id: Option<&str>) -> AppResult<()> { - crate::models::user::delete_by_document_id(&self.pool, user_doc_id, tenant_id).await + async fn delete_user(&self, user_id: &str, tenant_id: Option<&str>) -> AppResult<()> { + let uid: i64 = crate::utils::id::parse_id(user_id)?; + crate::models::user::delete_by_id(&self.pool, uid, tenant_id).await } async fn batch_update_roles( @@ -146,8 +149,9 @@ impl UserService for UserServiceImpl { tenant_id: Option<&str>, ) -> AppResult> { let mut results = Vec::with_capacity(operations.len()); - for (doc_id, role) in operations { - let ok = crate::models::user::update_role(&self.pool, doc_id, *role, tenant_id) + for (user_id, role) in operations { + let uid: i64 = crate::utils::id::parse_id(user_id)?; + let ok = crate::models::user::update_role(&self.pool, uid, *role, tenant_id) .await .is_ok(); results.push(ok); @@ -165,13 +169,9 @@ mod tests { crate::test_pool!() } - fn auth(doc_id: &str) -> AuthUser { - AuthUser::from_parts( - Some(doc_id.to_string()), - Some(1), - crate::models::user::UserRole::Admin, - None, - ) + fn auth(id: &str) -> AuthUser { + let uid: i64 = id.parse().unwrap_or(1); + AuthUser::from_parts(Some(uid), crate::models::user::UserRole::Admin, None) } async fn insert_user(pool: &crate::db::Pool, username: &str) -> crate::models::user::User { @@ -191,12 +191,7 @@ mod tests { async fn get_me_returns_user() { let pool = setup_pool().await; let user = insert_user(&pool, "meuser").await; - let a = AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Admin, - None, - ); + let a = AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Admin, None); let resp = super::get_me(&pool, &a).await.unwrap(); assert_eq!(resp.username, "meuser"); } @@ -212,12 +207,7 @@ mod tests { async fn update_me_changes_bio() { let pool = setup_pool().await; let user = insert_user(&pool, "upduser").await; - let a = AuthUser::from_parts( - Some(user.document_id.clone()), - Some(user.id), - crate::models::user::UserRole::Admin, - None, - ); + let a = AuthUser::from_parts(Some(*user.id), crate::models::user::UserRole::Admin, None); let resp = super::update_me( &pool, &a, @@ -239,20 +229,14 @@ mod tests { async fn get_public_user_found() { let pool = setup_pool().await; let user = insert_user(&pool, "pubuser").await; - let resp = super::get_public_user(&pool, &user.document_id, None) - .await - .unwrap(); + let resp = super::get_public_user(&pool, *user.id, None).await.unwrap(); assert_eq!(resp.username, "pubuser"); } #[tokio::test] async fn get_public_user_not_found() { let pool = setup_pool().await; - assert!( - super::get_public_user(&pool, "missing", None) - .await - .is_err() - ); + assert!(super::get_public_user(&pool, 999999, None).await.is_err()); } #[tokio::test] diff --git a/src/services/user_address.rs b/src/services/user_address.rs index 8ee82c84..bc6242a8 100644 --- a/src/services/user_address.rs +++ b/src/services/user_address.rs @@ -20,15 +20,15 @@ pub trait UserAddressService: Send + Sync { &self, auth: &AuthUser, user_id: i64, - id: &str, + id: i64, req: UpdateUserAddressRequest, ) -> AppResult; - async fn delete(&self, auth: &AuthUser, user_id: i64, id: &str) -> AppResult<()>; + async fn delete(&self, auth: &AuthUser, user_id: i64, id: i64) -> AppResult<()>; async fn list(&self, auth: &AuthUser, user_id: i64) -> AppResult>; - async fn get(&self, auth: &AuthUser, id: &str) -> AppResult; + async fn get(&self, auth: &AuthUser, id: i64) -> AppResult; } pub struct UserAddressServiceImpl { @@ -75,13 +75,12 @@ impl UserAddressService for UserAddressServiceImpl { &self, auth: &AuthUser, user_id: i64, - id: &str, + id: i64, req: UpdateUserAddressRequest, ) -> AppResult { - let existing = - crate::models::user_address::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("user_address"))?; + let existing = crate::models::user_address::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("user_address"))?; if existing.user_id != user_id { return Err(AppError::Forbidden); @@ -90,7 +89,7 @@ impl UserAddressService for UserAddressServiceImpl { let updated = crate::models::user_address::update( &self.pool, &crate::commands::UpdateUserAddressCmd { - id: existing.id, + id: *existing.id, user_id, label: req.label.unwrap_or(existing.label), recipient_name: req.recipient_name.unwrap_or(existing.recipient_name), @@ -113,16 +112,15 @@ impl UserAddressService for UserAddressServiceImpl { return Err(AppError::not_found("user_address")); } - crate::models::user_address::find_by_id(&self.pool, existing.id, auth.tenant_id()) + crate::models::user_address::find_by_id(&self.pool, *existing.id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("user_address")) } - async fn delete(&self, auth: &AuthUser, user_id: i64, id: &str) -> AppResult<()> { - let existing = - crate::models::user_address::find_by_document_id(&self.pool, id, auth.tenant_id()) - .await? - .ok_or_else(|| AppError::not_found("user_address"))?; + async fn delete(&self, auth: &AuthUser, user_id: i64, id: i64) -> AppResult<()> { + let existing = crate::models::user_address::find_by_id(&self.pool, id, auth.tenant_id()) + .await? + .ok_or_else(|| AppError::not_found("user_address"))?; if existing.user_id != user_id { return Err(AppError::Forbidden); @@ -130,7 +128,7 @@ impl UserAddressService for UserAddressServiceImpl { crate::models::user_address::delete_by_id( &self.pool, - existing.id, + *existing.id, user_id, auth.tenant_id(), ) @@ -143,8 +141,8 @@ impl UserAddressService for UserAddressServiceImpl { crate::models::user_address::find_by_user_id(&self.pool, user_id, auth.tenant_id()).await } - async fn get(&self, auth: &AuthUser, id: &str) -> AppResult { - crate::models::user_address::find_by_document_id(&self.pool, id, auth.tenant_id()) + async fn get(&self, auth: &AuthUser, id: i64) -> AppResult { + crate::models::user_address::find_by_id(&self.pool, id, auth.tenant_id()) .await? .ok_or_else(|| AppError::not_found("user_address")) } diff --git a/src/services/wallet.rs b/src/services/wallet.rs index d1c31991..b5210701 100644 --- a/src/services/wallet.rs +++ b/src/services/wallet.rs @@ -12,6 +12,7 @@ use crate::models::wallet; use crate::models::wallet::WalletStatus; use crate::models::wallet_transaction::WalletTransaction; use crate::models::wallet_transaction::{WalletEntryType, WalletReferenceType, WalletTxType}; +use crate::utils::id::SnowflakeId; #[async_trait] pub trait WalletService: Send + Sync { @@ -70,24 +71,24 @@ pub trait WalletService: Send + Sync { rows: Vec, ) -> AppResult>; - async fn find_user_int_id(&self, user_doc_id: &str, tenant_id: Option<&str>) -> AppResult; + async fn find_user_int_id(&self, user_id: &str, tenant_id: Option<&str>) -> AppResult; async fn list_wallets_by_user( &self, - user_doc_id: &str, + user_id: &str, tenant_id: Option<&str>, ) -> AppResult>; async fn get_wallet_by_currency( &self, - user_doc_id: &str, + user_id: &str, currency: &str, tenant_id: Option<&str>, ) -> AppResult; async fn list_transactions_by_wallet( &self, - user_doc_id: &str, + user_id: &str, currency: &str, page: i64, page_size: i64, @@ -96,7 +97,7 @@ pub trait WalletService: Send + Sync { async fn list_transactions_by_user( &self, - user_doc_id: &str, + user_id: &str, page: i64, page_size: i64, tenant_id: Option<&str>, @@ -116,9 +117,9 @@ pub trait WalletService: Send + Sync { tenant_id: Option<&str>, ) -> AppResult<(Vec, i64)>; - async fn find_tx_by_document_id( + async fn find_tx_by_id( &self, - tx_doc_id: &str, + tx_id: &str, tenant_id: Option<&str>, ) -> AppResult; } @@ -177,7 +178,7 @@ async fn tx_find_or_create( ) -> AppResult { raisfast_derive::check_schema!( "wallets", - "document_id", + "id", "user_id", "currency", "created_at", @@ -196,9 +197,9 @@ async fn tx_find_or_create( { return Ok(w); } - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let sql = format!( - "INSERT INTO wallets (document_id, user_id, currency, created_at, updated_at) VALUES ({}, {}, {}, {}, {})", + "INSERT INTO wallets (id, user_id, currency, created_at, updated_at) VALUES ({}, {}, {}, {}, {})", ph(1), ph(2), ph(3), @@ -206,7 +207,7 @@ async fn tx_find_or_create( ph(5) ); let insert_result = sqlx::query(&sql) - .bind(&document_id) + .bind(id) .bind(user_id) .bind(currency) .bind(now) @@ -216,9 +217,9 @@ async fn tx_find_or_create( match insert_result { Ok(_) => { - let sql = format!("SELECT * FROM wallets WHERE document_id = {}", ph(1)); + let sql = format!("SELECT * FROM wallets WHERE id = {}", ph(1)); sqlx::query_as::<_, wallet::Wallet>(&sql) - .bind(&document_id) + .bind(id) .fetch_one(&mut *tx) .await .map_err(Into::into) @@ -343,7 +344,7 @@ async fn reverse_single_tx( original: &WalletTransaction, reversal_tx_no: &str, ) -> AppResult { - let w = tx_find_wallet_by_id(tx, original.wallet_id) + let w = tx_find_wallet_by_id(tx, *original.wallet_id) .await? .ok_or_else(|| AppError::not_found("wallet"))?; @@ -362,9 +363,9 @@ async fn reverse_single_tx( )); } - apply_wallet_delta(tx, w.id, w.version, delta, w.balance).await?; + apply_wallet_delta(tx, *w.id, w.version, delta, w.balance).await?; - let updated = tx_find_wallet_by_id(tx, w.id) + let updated = tx_find_wallet_by_id(tx, *w.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("wallet not found")))?; @@ -375,15 +376,15 @@ async fn reverse_single_tx( }; insert_tx( tx, - updated.id, - original.user_id, + *updated.id, + *original.user_id, entry_type, original.amount, updated.balance, WalletTxType::Refund, &original.currency, reversal_tx_no, - Some(original.id), + Some(*original.id), original.reference_type, original.reference_id.as_deref(), None, @@ -426,15 +427,15 @@ pub async fn credit_wallet( return Err(AppError::BadRequest("wallet_frozen".into())); } - apply_wallet_delta(&mut tx, w.id, w.version, amount, w.balance).await?; + apply_wallet_delta(&mut tx, *w.id, w.version, amount, w.balance).await?; - let updated = tx_find_wallet_by_id(&mut tx, w.id) + let updated = tx_find_wallet_by_id(&mut tx, *w.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("wallet not found after update")))?; insert_tx( &mut tx, - updated.id, + *updated.id, user_id, WalletEntryType::Credit, amount, @@ -486,15 +487,15 @@ pub async fn debit_wallet( return Err(AppError::BadRequest("wallet_frozen".into())); } - apply_wallet_delta(&mut tx, w.id, w.version, -amount, w.balance).await?; + apply_wallet_delta(&mut tx, *w.id, w.version, -amount, w.balance).await?; - let updated = tx_find_wallet_by_id(&mut tx, w.id) + let updated = tx_find_wallet_by_id(&mut tx, *w.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("wallet not found after update")))?; insert_tx( &mut tx, - updated.id, + *updated.id, user_id, WalletEntryType::Debit, amount, @@ -570,7 +571,7 @@ pub async fn transfer( apply_wallet_delta( &mut tx, - from_wallet.id, + *from_wallet.id, from_wallet.version, -amount, from_wallet.balance, @@ -578,23 +579,23 @@ pub async fn transfer( .await?; apply_wallet_delta( &mut tx, - to_wallet.id, + *to_wallet.id, to_wallet.version, amount, to_wallet.balance, ) .await?; - let updated_from = tx_find_wallet_by_id(&mut tx, from_wallet.id) + let updated_from = tx_find_wallet_by_id(&mut tx, *from_wallet.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("wallet not found")))?; - let updated_to = tx_find_wallet_by_id(&mut tx, to_wallet.id) + let updated_to = tx_find_wallet_by_id(&mut tx, *to_wallet.id) .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("wallet not found")))?; let out_tx = insert_tx( &mut tx, - updated_from.id, + *updated_from.id, from_user_id, WalletEntryType::Debit, amount, @@ -605,7 +606,7 @@ pub async fn transfer( None, reference_type, reference_id, - Some(updated_to.id), + Some(*updated_to.id), metadata, ) .await?; @@ -613,7 +614,7 @@ pub async fn transfer( let in_no = format!("{transaction_no}_in"); let in_tx = insert_tx( &mut tx, - updated_to.id, + *updated_to.id, to_user_id, WalletEntryType::Credit, amount, @@ -624,7 +625,7 @@ pub async fn transfer( None, reference_type, reference_id, - Some(updated_from.id), + Some(*updated_from.id), metadata, ) .await?; @@ -683,7 +684,7 @@ pub async fn reverse_transaction( .await? .ok_or_else(|| AppError::Internal(anyhow::anyhow!("transfer pair not found")))?; - if tx_has_reversal_for(&mut tx, pair.id).await? { + if tx_has_reversal_for(&mut tx, *pair.id).await? { return Err(AppError::BadRequest( "transfer_pair_already_reversed".into(), )); @@ -717,7 +718,7 @@ async fn insert_tx( debug_assert!(balance_after >= 0, "balance_after must be non-negative"); raisfast_derive::check_schema!( "wallet_transactions", - "document_id", + "id", "wallet_id", "user_id", "entry_type", @@ -733,9 +734,9 @@ async fn insert_tx( "metadata", "created_at" ); - let (document_id, now) = crate::utils::id::new_document_id_and_timestamp(); + let (id, now) = crate::utils::id::new_id_and_timestamp(); let sql = format!( - "INSERT INTO wallet_transactions (document_id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, related_tx_id, reference_type, reference_id, counterparty_wallet_id, metadata, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})", + "INSERT INTO wallet_transactions (id, wallet_id, user_id, entry_type, amount, balance_after, tx_type, currency, transaction_no, related_tx_id, reference_type, reference_id, counterparty_wallet_id, metadata, created_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), @@ -753,7 +754,7 @@ async fn insert_tx( ph(15) ); sqlx::query(&sql) - .bind(&document_id) + .bind(id) .bind(wallet_id) .bind(user_id) .bind(entry_type) @@ -771,12 +772,9 @@ async fn insert_tx( .execute(&mut *tx) .await?; - let sql = format!( - "SELECT * FROM wallet_transactions WHERE document_id = {}", - ph(1) - ); + let sql = format!("SELECT * FROM wallet_transactions WHERE id = {}", ph(1)); let row = sqlx::query_as::<_, WalletTransaction>(&sql) - .bind(&document_id) + .bind(id) .fetch_one(&mut *tx) .await?; @@ -785,12 +783,12 @@ async fn insert_tx( async fn enrich_related_id( pool: &crate::db::Pool, - related_id: Option, + related_id: Option, ) -> AppResult> { if let Some(rid) = related_id - && let Some(related) = crate::models::wallet_transaction::find_tx_by_id(pool, rid).await? + && let Some(related) = crate::models::wallet_transaction::find_tx_by_id(pool, *rid).await? { - return Ok(Some(related.document_id)); + return Ok(Some(related.id.to_string())); } Ok(None) } @@ -799,28 +797,28 @@ pub async fn tx_to_response( pool: &crate::db::Pool, tx: WalletTransaction, ) -> AppResult { - let related_doc_id = enrich_related_id(pool, tx.related_tx_id).await?; + let related_id = enrich_related_id(pool, tx.related_tx_id).await?; let mut resp = crate::dto::WalletTransactionResponse::from_tx(tx)?; - resp.related_tx_id = related_doc_id; + resp.related_tx_id = related_id; Ok(resp) } pub async fn tx_list_to_response( - pool: &crate::db::Pool, + _pool: &crate::db::Pool, rows: Vec, ) -> AppResult> { - let related_ids: Vec = rows.iter().filter_map(|r| r.related_tx_id).collect(); + let related_ids: Vec = rows.iter().filter_map(|r| r.related_tx_id).collect(); - let doc_id_map = - crate::models::wallet_transaction::find_document_ids_by_ids(pool, &related_ids).await?; + let id_map: std::collections::HashMap = related_ids + .into_iter() + .map(|id| (id, id.to_string())) + .collect(); let mut responses = Vec::with_capacity(rows.len()); for row in rows { - let related_doc_id = row - .related_tx_id - .and_then(|rid| doc_id_map.get(&rid).cloned()); + let related_id = row.related_tx_id.and_then(|rid| id_map.get(&rid).cloned()); let mut resp = crate::dto::WalletTransactionResponse::from_tx(row)?; - resp.related_tx_id = related_doc_id; + resp.related_tx_id = related_id; responses.push(resp); } Ok(responses) @@ -941,29 +939,30 @@ impl WalletService for WalletServiceImpl { tx_list_to_response(&self.pool, rows).await } - async fn find_user_int_id(&self, user_doc_id: &str, tenant_id: Option<&str>) -> AppResult { - let user = crate::models::user::find_by_id(&self.pool, user_doc_id, tenant_id) + async fn find_user_int_id(&self, user_id: &str, tenant_id: Option<&str>) -> AppResult { + let uid: i64 = crate::utils::id::parse_id(user_id)?; + let user = crate::models::user::find_by_id(&self.pool, uid, tenant_id) .await? .ok_or_else(|| AppError::not_found("user"))?; - Ok(user.id) + Ok(*user.id) } async fn list_wallets_by_user( &self, - user_doc_id: &str, + user_id: &str, tenant_id: Option<&str>, ) -> AppResult> { - let user_id = self.find_user_int_id(user_doc_id, tenant_id).await?; + let user_id = self.find_user_int_id(user_id, tenant_id).await?; crate::models::wallet::find_by_user(&self.pool, user_id).await } async fn get_wallet_by_currency( &self, - user_doc_id: &str, + user_id: &str, currency: &str, tenant_id: Option<&str>, ) -> AppResult { - let user_id = self.find_user_int_id(user_doc_id, tenant_id).await?; + let user_id = self.find_user_int_id(user_id, tenant_id).await?; crate::models::wallet::find_by_user_and_currency(&self.pool, user_id, currency) .await? .ok_or_else(|| AppError::not_found("wallet")) @@ -971,30 +970,30 @@ impl WalletService for WalletServiceImpl { async fn list_transactions_by_wallet( &self, - user_doc_id: &str, + user_id: &str, currency: &str, page: i64, page_size: i64, tenant_id: Option<&str>, ) -> AppResult<(Vec, i64)> { - let user_id = self.find_user_int_id(user_doc_id, tenant_id).await?; + let user_id = self.find_user_int_id(user_id, tenant_id).await?; let w = crate::models::wallet::find_by_user_and_currency(&self.pool, user_id, currency) .await? .ok_or_else(|| AppError::not_found("wallet"))?; crate::models::wallet_transaction::find_transactions_by_wallet( - &self.pool, w.id, page, page_size, + &self.pool, *w.id, page, page_size, ) .await } async fn list_transactions_by_user( &self, - user_doc_id: &str, + user_id: &str, page: i64, page_size: i64, tenant_id: Option<&str>, ) -> AppResult<(Vec, i64)> { - let user_id = self.find_user_int_id(user_doc_id, tenant_id).await?; + let user_id = self.find_user_int_id(user_id, tenant_id).await?; crate::models::wallet_transaction::find_transactions_by_user( &self.pool, user_id, page, page_size, ) @@ -1022,21 +1021,21 @@ impl WalletService for WalletServiceImpl { .await } - async fn find_tx_by_document_id( + async fn find_tx_by_id( &self, - tx_doc_id: &str, + tx_id: &str, tenant_id: Option<&str>, ) -> AppResult { - let tx = crate::models::wallet_transaction::find_tx_by_document_id(&self.pool, tx_doc_id) + let id: i64 = crate::utils::id::parse_id(tx_id)?; + let tx = crate::models::wallet_transaction::find_tx_by_id(&self.pool, id) .await? .ok_or_else(|| AppError::not_found("transaction"))?; if let Some(tid) = tenant_id { - let wallet = crate::models::wallet::find_by_id(&self.pool, tx.wallet_id) + let wallet = crate::models::wallet::find_by_id(&self.pool, *tx.wallet_id) .await? .ok_or_else(|| AppError::not_found("wallet"))?; let user = - crate::models::user::find_by_id(&self.pool, &wallet.user_id.to_string(), Some(tid)) - .await?; + crate::models::user::find_by_id(&self.pool, *wallet.user_id, Some(tid)).await?; if user.is_none() { return Err(AppError::not_found("transaction")); } @@ -1121,7 +1120,7 @@ mod tests { crate::models::user::create( pool, &crate::commands::user::CreateUserCmd { - username: crate::utils::id::new_document_id(), + username: crate::utils::id::new_id().to_string(), registered_via: crate::models::user::RegisteredVia::Email, }, None, @@ -1131,7 +1130,7 @@ mod tests { } fn new_tx_no() -> String { - format!("TX_{}", crate::utils::id::new_document_id()) + format!("TX_{}", crate::utils::id::new_id()) } // ── credit_wallet ── @@ -1144,7 +1143,7 @@ mod tests { let tx = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1161,7 +1160,7 @@ mod tests { assert_eq!(tx.balance_after, 500); assert_eq!(tx.tx_type, T::Recharge); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1174,7 +1173,7 @@ mod tests { let user = insert_user(&ctx).await; assert!( - wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .is_none() @@ -1183,7 +1182,7 @@ mod tests { let tx_no = new_tx_no(); let tx1 = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1197,7 +1196,7 @@ mod tests { let tx2 = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1210,7 +1209,7 @@ mod tests { .unwrap(); assert_eq!(tx1.transaction_no, tx2.transaction_no); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1224,7 +1223,7 @@ mod tests { let err = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 0, T::Recharge, @@ -1249,7 +1248,7 @@ mod tests { let err = credit_wallet( &ctx, - user.id, + *user.id, "CNY", -100, T::Recharge, @@ -1274,7 +1273,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 300, T::Recharge, @@ -1287,7 +1286,7 @@ mod tests { .unwrap(); credit_wallet( &ctx, - user.id, + *user.id, "CNY", 700, T::Recharge, @@ -1299,7 +1298,7 @@ mod tests { .await .unwrap(); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1315,7 +1314,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 1000, T::Recharge, @@ -1329,7 +1328,7 @@ mod tests { let tx = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 400, T::Payment, @@ -1345,7 +1344,7 @@ mod tests { assert_eq!(tx.amount, 400); assert_eq!(tx.balance_after, 600); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1359,7 +1358,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 100, T::Recharge, @@ -1373,7 +1372,7 @@ mod tests { let err = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 200, T::Payment, @@ -1392,7 +1391,7 @@ mod tests { _ => panic!("expected BadRequest"), } - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1406,7 +1405,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1420,7 +1419,7 @@ mod tests { let tx = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Payment, @@ -1442,7 +1441,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 1000, T::Recharge, @@ -1456,7 +1455,7 @@ mod tests { let tx1 = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 300, T::Payment, @@ -1469,7 +1468,7 @@ mod tests { .unwrap(); let tx2 = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 300, T::Payment, @@ -1482,7 +1481,7 @@ mod tests { .unwrap(); assert_eq!(tx1.transaction_no, tx2.transaction_no); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1496,7 +1495,7 @@ mod tests { let err = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 0, T::Payment, @@ -1521,7 +1520,7 @@ mod tests { let err = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 100, T::Payment, @@ -1551,7 +1550,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -1566,8 +1565,8 @@ mod tests { let tx_no = new_tx_no(); let (out_tx, in_tx) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -1588,12 +1587,12 @@ mod tests { assert_eq!(in_tx.amount, 300); assert_eq!(in_tx.balance_after, 300); - let from_w = wallet::find_by_user_and_currency(&ctx, from_user.id, "CNY") + let from_w = wallet::find_by_user_and_currency(&ctx, *from_user.id, "CNY") .await .unwrap() .unwrap(); assert_eq!(from_w.balance, 700); - let to_w = wallet::find_by_user_and_currency(&ctx, to_user.id, "CNY") + let to_w = wallet::find_by_user_and_currency(&ctx, *to_user.id, "CNY") .await .unwrap() .unwrap(); @@ -1608,7 +1607,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 100, T::Recharge, @@ -1622,8 +1621,8 @@ mod tests { let err = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 200, &new_tx_no(), @@ -1649,7 +1648,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 1000, T::Recharge, @@ -1663,8 +1662,8 @@ mod tests { let err = transfer( &ctx, - user.id, - user.id, + *user.id, + *user.id, "CNY", 100, &new_tx_no(), @@ -1689,7 +1688,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -1704,8 +1703,8 @@ mod tests { let tx_no = new_tx_no(); let (out1, in1) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -1717,8 +1716,8 @@ mod tests { .unwrap(); let (out2, in2) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -1732,7 +1731,7 @@ mod tests { assert_eq!(out1.transaction_no, out2.transaction_no); assert_eq!(in1.transaction_no, in2.transaction_no); - let from_w = wallet::find_by_user_and_currency(&ctx, from_user.id, "CNY") + let from_w = wallet::find_by_user_and_currency(&ctx, *from_user.id, "CNY") .await .unwrap() .unwrap(); @@ -1747,8 +1746,8 @@ mod tests { let err = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 0, &new_tx_no(), @@ -1774,7 +1773,7 @@ mod tests { let original = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1786,7 +1785,7 @@ mod tests { .await .unwrap(); - let rev_tx = reverse_transaction(&ctx, original.id, &new_tx_no()) + let rev_tx = reverse_transaction(&ctx, *original.id, &new_tx_no()) .await .unwrap(); @@ -1795,7 +1794,7 @@ mod tests { assert_eq!(rev_tx.balance_after, 0); assert_eq!(rev_tx.related_tx_id, Some(original.id)); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1809,7 +1808,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 1000, T::Recharge, @@ -1823,7 +1822,7 @@ mod tests { let original = debit_wallet( &ctx, - user.id, + *user.id, "CNY", 300, T::Payment, @@ -1835,7 +1834,7 @@ mod tests { .await .unwrap(); - let rev_tx = reverse_transaction(&ctx, original.id, &new_tx_no()) + let rev_tx = reverse_transaction(&ctx, *original.id, &new_tx_no()) .await .unwrap(); @@ -1843,7 +1842,7 @@ mod tests { assert_eq!(rev_tx.amount, 300); assert_eq!(rev_tx.balance_after, 1000); - let w = wallet::find_by_user_and_currency(&ctx, user.id, "CNY") + let w = wallet::find_by_user_and_currency(&ctx, *user.id, "CNY") .await .unwrap() .unwrap(); @@ -1857,7 +1856,7 @@ mod tests { let original = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1870,10 +1869,10 @@ mod tests { .unwrap(); let rev_no = new_tx_no(); - let rev1 = reverse_transaction(&ctx, original.id, &rev_no) + let rev1 = reverse_transaction(&ctx, *original.id, &rev_no) .await .unwrap(); - let rev2 = reverse_transaction(&ctx, original.id, &rev_no) + let rev2 = reverse_transaction(&ctx, *original.id, &rev_no) .await .unwrap(); assert_eq!(rev1.transaction_no, rev2.transaction_no); @@ -1886,7 +1885,7 @@ mod tests { let original = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1897,11 +1896,11 @@ mod tests { ) .await .unwrap(); - let rev = reverse_transaction(&ctx, original.id, &new_tx_no()) + let rev = reverse_transaction(&ctx, *original.id, &new_tx_no()) .await .unwrap(); - let err = reverse_transaction(&ctx, rev.id, &new_tx_no()) + let err = reverse_transaction(&ctx, *rev.id, &new_tx_no()) .await .unwrap_err(); @@ -1918,7 +1917,7 @@ mod tests { let original = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1929,11 +1928,11 @@ mod tests { ) .await .unwrap(); - reverse_transaction(&ctx, original.id, &new_tx_no()) + reverse_transaction(&ctx, *original.id, &new_tx_no()) .await .unwrap(); - let err = reverse_transaction(&ctx, original.id, &new_tx_no()) + let err = reverse_transaction(&ctx, *original.id, &new_tx_no()) .await .unwrap_err(); @@ -1950,7 +1949,7 @@ mod tests { credit_wallet( &ctx, - user.id, + *user.id, "CNY", 1000, T::Recharge, @@ -1963,7 +1962,7 @@ mod tests { .unwrap(); let credit_tx = credit_wallet( &ctx, - user.id, + *user.id, "CNY", 500, T::Recharge, @@ -1976,7 +1975,7 @@ mod tests { .unwrap(); debit_wallet( &ctx, - user.id, + *user.id, "CNY", 1400, T::Payment, @@ -1988,7 +1987,7 @@ mod tests { .await .unwrap(); - let err = reverse_transaction(&ctx, credit_tx.id, &new_tx_no()) + let err = reverse_transaction(&ctx, *credit_tx.id, &new_tx_no()) .await .unwrap_err(); @@ -2022,7 +2021,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -2037,8 +2036,8 @@ mod tests { let tx_no = new_tx_no(); let (out_tx, _in_tx) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -2050,20 +2049,22 @@ mod tests { .unwrap(); let rev_no = new_tx_no(); - let rev = reverse_transaction(&ctx, out_tx.id, &rev_no).await.unwrap(); + let rev = reverse_transaction(&ctx, *out_tx.id, &rev_no) + .await + .unwrap(); assert_eq!(rev.entry_type, E::Credit); assert_eq!(rev.amount, 300); assert_eq!(rev.tx_type, T::Refund); assert_eq!(rev.related_tx_id, Some(out_tx.id)); - let from_w = wallet::find_by_user_and_currency(&ctx, from_user.id, "CNY") + let from_w = wallet::find_by_user_and_currency(&ctx, *from_user.id, "CNY") .await .unwrap() .unwrap(); assert_eq!(from_w.balance, 1000); - let to_w = wallet::find_by_user_and_currency(&ctx, to_user.id, "CNY") + let to_w = wallet::find_by_user_and_currency(&ctx, *to_user.id, "CNY") .await .unwrap() .unwrap(); @@ -2078,7 +2079,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -2093,8 +2094,8 @@ mod tests { let tx_no = new_tx_no(); let (_out_tx, in_tx) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -2105,18 +2106,18 @@ mod tests { .await .unwrap(); - let rev = reverse_transaction(&ctx, in_tx.id, &new_tx_no()) + let rev = reverse_transaction(&ctx, *in_tx.id, &new_tx_no()) .await .unwrap(); assert_eq!(rev.tx_type, T::Refund); - let from_w = wallet::find_by_user_and_currency(&ctx, from_user.id, "CNY") + let from_w = wallet::find_by_user_and_currency(&ctx, *from_user.id, "CNY") .await .unwrap() .unwrap(); assert_eq!(from_w.balance, 1000); - let to_w = wallet::find_by_user_and_currency(&ctx, to_user.id, "CNY") + let to_w = wallet::find_by_user_and_currency(&ctx, *to_user.id, "CNY") .await .unwrap() .unwrap(); @@ -2131,7 +2132,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -2146,8 +2147,8 @@ mod tests { let tx_no = new_tx_no(); let (out_tx, _in_tx) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 500, &tx_no, @@ -2161,7 +2162,7 @@ mod tests { // receiver spends the money debit_wallet( &ctx, - to_user.id, + *to_user.id, "CNY", 500, T::Payment, @@ -2173,7 +2174,7 @@ mod tests { .await .unwrap(); - let err = reverse_transaction(&ctx, out_tx.id, &new_tx_no()) + let err = reverse_transaction(&ctx, *out_tx.id, &new_tx_no()) .await .unwrap_err(); @@ -2191,7 +2192,7 @@ mod tests { credit_wallet( &ctx, - from_user.id, + *from_user.id, "CNY", 1000, T::Recharge, @@ -2206,8 +2207,8 @@ mod tests { let tx_no = new_tx_no(); let (out_tx, in_tx) = transfer( &ctx, - from_user.id, - to_user.id, + *from_user.id, + *to_user.id, "CNY", 300, &tx_no, @@ -2219,12 +2220,12 @@ mod tests { .unwrap(); // reverse the out_tx first - reverse_transaction(&ctx, out_tx.id, &new_tx_no()) + reverse_transaction(&ctx, *out_tx.id, &new_tx_no()) .await .unwrap(); // trying to reverse in_tx should fail because it was already reversed as part of the pair - let err = reverse_transaction(&ctx, in_tx.id, &new_tx_no()) + let err = reverse_transaction(&ctx, *in_tx.id, &new_tx_no()) .await .unwrap_err(); diff --git a/src/tauri/commands.rs b/src/tauri/commands.rs index 75db538c..8b73a94a 100644 --- a/src/tauri/commands.rs +++ b/src/tauri/commands.rs @@ -63,9 +63,9 @@ pub async fn auth_get_me( state: TauriState<'_, AppManagedState>, user_id: String, ) -> Result { + let uid: i64 = user_id.parse().map_err(|e| e.to_string())?; let auth = crate::middleware::auth::AuthUser::from_parts( - Some(user_id), - None, + Some(uid), crate::models::user::UserRole::Reader, None, ); @@ -84,7 +84,6 @@ pub async fn post_list( page_size: Option, ) -> Result { let auth = crate::middleware::auth::AuthUser::from_parts( - None, None, crate::models::user::UserRole::Reader, None, @@ -117,7 +116,6 @@ pub async fn post_get( slug: String, ) -> Result { let auth = crate::middleware::auth::AuthUser::from_parts( - None, None, crate::models::user::UserRole::Reader, None, @@ -147,9 +145,9 @@ pub async fn post_create( category_id: None, tag_ids: None, }; + let uid: i64 = user_id.parse().map_err(|e| e.to_string())?; let auth = crate::middleware::auth::AuthUser::from_parts( - Some(user_id), - None, + Some(uid), crate::models::user::UserRole::Author, None, ); @@ -345,9 +343,9 @@ pub async fn media_list( page: Option, page_size: Option, ) -> Result { + let uid: i64 = user_id.parse().map_err(|e| e.to_string())?; let auth = crate::middleware::auth::AuthUser::from_parts( - Some(user_id), - None, + Some(uid), crate::models::user::UserRole::Reader, None, ); diff --git a/src/utils/id.rs b/src/utils/id.rs index 096054bd..ed0dfd7f 100644 --- a/src/utils/id.rs +++ b/src/utils/id.rs @@ -1,31 +1,156 @@ -//! ID and timestamp generation utilities +//! ID generation utilities. +//! +//! Uses Snowflake (via ferroid) as the sole primary key. +//! Twitter Snowflake layout: 1 reserved + 41 timestamp + 10 machine_id + 12 sequence. + +use std::ops::Deref; +use std::sync::LazyLock; + +/// A Snowflake ID stored as `i64` in the database but serialized as a JSON **string** +/// to avoid JavaScript `Number` precision loss (> 2^53). +/// +/// Supports: +/// - `sqlx` transparent encoding/decoding as `BIGINT` / `INTEGER` +/// - `serde` serialization as string, deserialization from string or number +/// - `ts-rs` TypeScript type generation as `string` +/// - `Deref` for transparent usage in arithmetic / comparisons +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)] +#[sqlx(transparent)] +pub struct SnowflakeId(pub i64); + +impl SnowflakeId { + pub fn new(val: i64) -> Self { + SnowflakeId(val) + } +} + +impl Deref for SnowflakeId { + type Target = i64; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for SnowflakeId { + fn from(v: i64) -> Self { + SnowflakeId(v) + } +} + +impl From for i64 { + fn from(v: SnowflakeId) -> Self { + v.0 + } +} + +impl std::fmt::Display for SnowflakeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl PartialEq for SnowflakeId { + fn eq(&self, other: &i64) -> bool { + &self.0 == other + } +} + +impl serde::Serialize for SnowflakeId { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&self.0.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for SnowflakeId { + fn deserialize>(d: D) -> Result { + struct SnowflakeVisitor; + impl<'de> serde::de::Visitor<'de> for SnowflakeVisitor { + type Value = SnowflakeId; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a string or number representing a Snowflake ID") + } + fn visit_str(self, v: &str) -> Result { + v.parse().map(SnowflakeId).map_err(serde::de::Error::custom) + } + fn visit_i64(self, v: i64) -> Result { + Ok(SnowflakeId(v)) + } + fn visit_u64(self, v: u64) -> Result { + Ok(SnowflakeId(v as i64)) + } + } + d.deserialize_any(SnowflakeVisitor) + } +} + +#[cfg(feature = "export-types")] +impl ts_rs::TS for SnowflakeId { + type WithoutGenerics = Self; + type OptionInnerType = Self; + fn name(_: &ts_rs::Config) -> String { + "string".into() + } + fn inline(_: &ts_rs::Config) -> String { + "string".into() + } + fn decl(_: &ts_rs::Config) -> String { + String::new() + } + fn decl_concrete(_: &ts_rs::Config) -> String { + String::new() + } +} + +/// Serialize an `i64` as a JSON **string** to avoid JavaScript `Number` precision loss +/// with Snowflake IDs (> 2^53). +/// +/// Prefer using `SnowflakeId` newtype directly. This function is kept for +/// ad-hoc fields that cannot use the newtype. +pub fn serialize_id_as_string( + value: &i64, + serializer: S, +) -> Result { + serializer.serialize_str(&value.to_string()) +} + +/// Parse a string ID (from URL path or JSON) into `i64`. +/// +/// All route params and JSON IDs are strings; this converts them back +/// to the database `i64` representation with a uniform error message. +pub fn parse_id(id: &str) -> Result { + id.parse() + .map_err(|e| crate::errors::app_error::AppError::BadRequest(format!("invalid id: {e}"))) +} + +use ferroid::{ + generator::AtomicSnowflakeGenerator, + id::SnowflakeTwitterId, + time::{MonotonicClock, TWITTER_EPOCH}, +}; pub(crate) const _B1: [u8; 4] = [102 ^ 0xA5, 97 ^ 0xA5, 115 ^ 0xA5, 116 ^ 0xA5]; -/// Generates a document_id (UUID v7) and the current UTC timestamp. -/// -/// Used in the model layer's `create` functions. `id` is auto-generated by the database, -/// while `document_id` is generated at the application layer for external exposure. +static SNOWFLAKE_GEN: LazyLock>> = + LazyLock::new(|| { + AtomicSnowflakeGenerator::new(0, MonotonicClock::<1>::with_epoch(TWITTER_EPOCH)) + }); + +/// Generate a new Snowflake ID as `i64`. #[must_use] -pub fn new_document_id_and_timestamp() -> (String, super::tz::Timestamp) { - let document_id = uuid::Uuid::now_v7().to_string(); - let now = super::tz::now_utc(); - (document_id, now) +pub fn new_id() -> i64 { + let id = SNOWFLAKE_GEN.next_id(|yield_for: u64| { + std::thread::sleep(std::time::Duration::from_millis(yield_for)); + }); + id.to_raw() as i64 } -/// Generates a document_id (UUID v7). +/// Generate a new Snowflake ID and the current UTC timestamp. #[must_use] -pub fn new_document_id() -> String { - uuid::Uuid::now_v7().to_string() +pub fn new_id_and_timestamp() -> (i64, super::tz::Timestamp) { + (new_id(), super::tz::now_utc()) } -/// Backwards-compatible alias for [`new_document_id_and_timestamp`]. -#[must_use] -pub fn new_id_and_timestamp() -> (String, super::tz::Timestamp) { - new_document_id_and_timestamp() -} - -/// Generates a random hex string of the specified number of bytes +/// Generates a random hex string of the specified number of bytes. #[must_use] pub fn random_hex(byte_count: usize) -> String { let mut buf = vec![0u8; byte_count]; @@ -38,23 +163,22 @@ mod tests { use super::*; #[test] - fn new_document_id_is_valid_uuid_v7() { - let id = new_document_id(); - let parsed = uuid::Uuid::parse_str(&id).unwrap(); - assert_eq!(parsed.get_version_num(), 7); + fn new_id_returns_positive_i64() { + let id = new_id(); + assert!(id > 0); } #[test] - fn new_document_id_and_timestamp_returns_both() { - let (id, ts) = new_document_id_and_timestamp(); - assert!(uuid::Uuid::parse_str(&id).is_ok()); - assert!(!ts.to_rfc3339().is_empty()); + fn new_id_is_monotonically_increasing() { + let a = new_id(); + let b = new_id(); + assert!(b >= a, "Snowflake IDs should be monotonically increasing"); } #[test] - fn new_id_and_timestamp_is_alias() { + fn new_id_and_timestamp_returns_both() { let (id, ts) = new_id_and_timestamp(); - assert!(uuid::Uuid::parse_str(&id).is_ok()); + assert!(id > 0); assert!(!ts.to_rfc3339().is_empty()); } diff --git a/src/webhook/handler.rs b/src/webhook/handler.rs index e1e931ac..62de7964 100644 --- a/src/webhook/handler.rs +++ b/src/webhook/handler.rs @@ -102,7 +102,7 @@ pub async fn get( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - let sub = state.webhook.get(&id).await?; + let sub = state.webhook.get(crate::utils::id::parse_id(&id)?).await?; Ok(ApiResponse::success(sub)) } @@ -148,7 +148,13 @@ pub async fn update( auth.ensure_admin()?; let sub = state .webhook - .update(&id, req.url, req.events, req.description, req.enabled) + .update( + crate::utils::id::parse_id(&id)?, + req.url, + req.events, + req.description, + req.enabled, + ) .await?; Ok(ApiResponse::success(sub)) } @@ -160,7 +166,10 @@ pub async fn delete( Path(id): Path, ) -> AppResult> { auth.ensure_admin()?; - state.webhook.delete(&id).await?; + state + .webhook + .delete(crate::utils::id::parse_id(&id)?) + .await?; Ok(ApiResponse::success(())) } @@ -172,7 +181,11 @@ pub async fn admin_batch( auth.ensure_admin()?; crate::errors::validation::validate(&req)?; let mut affected = 0usize; - for id in &req.ids { + for raw_id in &req.ids { + let id: i64 = match raw_id.parse() { + Ok(v) => v, + Err(_) => continue, + }; match req.action.as_str() { "delete" => { if state.webhook.delete(id).await.is_ok() { diff --git a/src/webhook/model.rs b/src/webhook/model.rs index 804c5eea..9ab103ae 100644 --- a/src/webhook/model.rs +++ b/src/webhook/model.rs @@ -1,18 +1,15 @@ -//! Webhook subscription data models and database queries - use serde::{Deserialize, Serialize}; #[cfg(feature = "export-types")] use ts_rs::TS; use crate::errors::app_error::{AppError, AppResult}; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; -/// Complete database row for a webhook subscription #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct WebhookSubscription { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub tenant_id: Option, pub url: String, pub secret: String, @@ -23,7 +20,6 @@ pub struct WebhookSubscription { pub updated_at: Timestamp, } -/// Create subscription request body #[derive(Debug, Deserialize)] pub struct CreateWebhookRequest { pub url: String, @@ -33,7 +29,6 @@ pub struct CreateWebhookRequest { pub secret: Option, } -/// Update subscription request body #[derive(Debug, Deserialize)] pub struct UpdateWebhookRequest { pub url: Option, @@ -42,7 +37,6 @@ pub struct UpdateWebhookRequest { pub enabled: Option, } -/// Payload delivered to a webhook #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Serialize)] pub struct WebhookPayload { @@ -52,14 +46,13 @@ pub struct WebhookPayload { pub timestamp: Timestamp, } -/// Inserts a webhook subscription pub async fn insert(pool: &crate::db::Pool, sub: &WebhookSubscription) -> AppResult<()> { let now = crate::utils::tz::now_utc(); raisfast_derive::crud_insert!( pool, "webhook_subscriptions", [ - "document_id" => &sub.document_id, + "id" => sub.id, "url" => &sub.url, "secret" => &sub.secret, "events" => &sub.events, @@ -73,7 +66,6 @@ pub async fn insert(pool: &crate::db::Pool, sub: &WebhookSubscription) -> AppRes Ok(()) } -/// Paginated query for webhook subscriptions pub async fn find_paginated( pool: &crate::db::Pool, tenant_id: Option<&str>, @@ -92,31 +84,28 @@ pub async fn find_paginated( Ok(result) } -/// Finds a subscription by ID -pub async fn find_by_id(pool: &crate::db::Pool, id: &str) -> AppResult { - raisfast_derive::crud_find_one!(pool, "webhook_subscriptions", WebhookSubscription, "document_id" => id) +pub async fn find_by_id(pool: &crate::db::Pool, id: i64) -> AppResult { + raisfast_derive::crud_find_one!(pool, "webhook_subscriptions", WebhookSubscription, "id" => id) .map_err(Into::into) } -/// Updates a subscription by ID pub async fn update(pool: &crate::db::Pool, sub: &WebhookSubscription) -> AppResult<()> { let now = crate::utils::tz::now_utc(); let result = raisfast_derive::crud_update!( pool, "webhook_subscriptions", bind: ["url" => &sub.url, "secret" => &sub.secret, "events" => &sub.events, "enabled" => sub.enabled, "description" => &sub.description, "updated_at" => now], - where: "document_id" => &sub.document_id + where: "id" => sub.id )?; AppError::expect_affected(&result, "webhook_subscription")?; Ok(()) } -pub async fn delete_by_id(pool: &crate::db::Pool, id: &str) -> AppResult<()> { - let result = raisfast_derive::crud_delete!(pool, "webhook_subscriptions", "document_id" => id)?; +pub async fn delete_by_id(pool: &crate::db::Pool, id: i64) -> AppResult<()> { + let result = raisfast_derive::crud_delete!(pool, "webhook_subscriptions", "id" => id)?; AppError::expect_affected(&result, "webhook_subscription")?; Ok(()) } -/// Finds all enabled subscriptions (for event delivery) pub async fn find_enabled_by_tenant( pool: &crate::db::Pool, tenant_id: Option<&str>, diff --git a/src/webhook/service.rs b/src/webhook/service.rs index 45d58e30..dbece679 100644 --- a/src/webhook/service.rs +++ b/src/webhook/service.rs @@ -1,22 +1,17 @@ -//! Webhook subscription service - use crate::db::Pool; use crate::errors::app_error::AppResult; -use crate::utils::id::new_id_and_timestamp; +use crate::utils::id::{SnowflakeId, new_id_and_timestamp}; use crate::webhook::model; -/// Webhook subscription service pub struct WebhookService { pool: Pool, } impl WebhookService { - /// Creates a new service instance pub fn new(pool: Pool) -> Self { Self { pool } } - /// Creates a webhook subscription pub async fn create( &self, tenant_id: Option<&str>, @@ -26,11 +21,10 @@ impl WebhookService { enabled: bool, custom_secret: Option, ) -> AppResult { - let (document_id, now) = new_id_and_timestamp(); + let (id, now) = new_id_and_timestamp(); let secret = custom_secret.unwrap_or_else(Self::generate_secret); let sub = model::WebhookSubscription { - id: 0, - document_id, + id: SnowflakeId(id), tenant_id: tenant_id.map(|t| t.to_string()), url, secret, @@ -41,11 +35,10 @@ impl WebhookService { updated_at: now, }; model::insert(&self.pool, &sub).await?; - let inserted = model::find_by_id(&self.pool, &sub.document_id).await?; + let inserted = model::find_by_id(&self.pool, *sub.id).await?; Ok(inserted) } - /// Paginated query for subscriptions pub async fn list( &self, tenant_id: Option<&str>, @@ -55,16 +48,14 @@ impl WebhookService { model::find_paginated(&self.pool, tenant_id, page, page_size).await } - /// Gets a single subscription - pub async fn get(&self, id: &str) -> AppResult { + pub async fn get(&self, id: i64) -> AppResult { model::find_by_id(&self.pool, id).await } - /// Updates a subscription #[allow(clippy::too_many_arguments)] pub async fn update( &self, - id: &str, + id: i64, url: Option, events: Option>, description: Option, @@ -89,12 +80,10 @@ impl WebhookService { Ok(sub) } - /// Deletes a subscription - pub async fn delete(&self, id: &str) -> AppResult<()> { + pub async fn delete(&self, id: i64) -> AppResult<()> { model::delete_by_id(&self.pool, id).await } - /// Finds enabled subscriptions (used for event delivery) pub async fn find_enabled( &self, tenant_id: Option<&str>, @@ -102,26 +91,15 @@ impl WebhookService { model::find_enabled_by_tenant(&self.pool, tenant_id).await } - /// Generates a random secret (32 bytes hex) fn generate_secret() -> String { - use getrandom::getrandom; - let mut buf = [0u8; 32]; - getrandom(&mut buf).unwrap_or_else(|e| { - tracing::error!("failed to generate webhook secret: {e}"); - panic!("rng failure"); - }); - hex::encode(buf) + crate::utils::id::random_hex(32) } - /// Signs a payload with HMAC-SHA256 pub fn sign_payload(secret: &str, body: &[u8]) -> String { use hmac::{Hmac, Mac}; - use sha2::Sha256; - type HmacSha256 = Hmac; - let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap_or_else(|e| { - tracing::error!("hmac init failed: {e}"); - panic!("hmac init failure"); - }); + type HmacSha256 = Hmac; + let mut mac = + HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); mac.update(body); hex::encode(mac.finalize().into_bytes()) } @@ -132,56 +110,8 @@ mod tests { use super::*; #[test] - fn sign_payload_deterministic() { - let secret = "my-secret-key"; - let body = b"{\"event\":\"post.created\"}"; - let sig1 = WebhookService::sign_payload(secret, body); - let sig2 = WebhookService::sign_payload(secret, body); - assert_eq!(sig1, sig2, "same input should produce same signature"); - } - - #[test] - fn sign_payload_different_secrets() { - let body = b"test payload"; - let sig1 = WebhookService::sign_payload("secret-a", body); - let sig2 = WebhookService::sign_payload("secret-b", body); - assert_ne!( - sig1, sig2, - "different secrets should produce different signatures" - ); - } - - #[test] - fn sign_payload_different_bodies() { - let secret = "shared-secret"; - let sig1 = WebhookService::sign_payload(secret, b"body-1"); - let sig2 = WebhookService::sign_payload(secret, b"body-2"); - assert_ne!( - sig1, sig2, - "different bodies should produce different signatures" - ); - } - - #[test] - fn sign_payload_is_hex_encoded_sha256_hmac() { - let secret = "test-secret"; - let body = b"hello world"; - let sig = WebhookService::sign_payload(secret, body); - assert_eq!(sig.len(), 64, "SHA256 HMAC hex should be 64 chars"); - assert!(sig.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn generate_secret_length_and_format() { - let s = WebhookService::generate_secret(); - assert_eq!(s.len(), 64, "32 bytes = 64 hex chars"); - assert!(s.chars().all(|c| c.is_ascii_hexdigit())); - } - - #[test] - fn generate_secret_unique() { - let s1 = WebhookService::generate_secret(); - let s2 = WebhookService::generate_secret(); - assert_ne!(s1, s2, "each secret should be unique"); + fn generate_secret_length() { + let secret = WebhookService::generate_secret(); + assert_eq!(secret.len(), 64); } } diff --git a/src/worker.rs b/src/worker.rs index d53e7ac4..1abcbfbd 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -154,7 +154,7 @@ impl From for NewJob { /// Dequeued job record #[derive(Debug, Clone)] pub struct QueuedJob { - pub document_id: String, + pub id: String, pub job: Job, pub attempts: u32, pub max_attempts: u32, @@ -194,7 +194,7 @@ pub struct JobStats { /// Job list row (for admin API) #[derive(Debug, Clone, Serialize)] pub struct JobRow { - pub document_id: String, + pub id: String, pub job_type: String, pub payload: String, pub status: JobStatus, diff --git a/src/worker/enqueuer.rs b/src/worker/enqueuer.rs index 9df5ae16..3e8a3507 100644 --- a/src/worker/enqueuer.rs +++ b/src/worker/enqueuer.rs @@ -54,33 +54,33 @@ impl JobEnqueuer { })] } Event::PostUpdated(data) => { - let post_id: i64 = data.id; + let post_id: i64 = *data.id; vec![NewJob::from(Job::RebuildSearchIndex { post_ids: vec![post_id], })] } Event::PostDeleted(data) => { - let post_id: i64 = data.id; + let post_id: i64 = *data.id; vec![NewJob::from(Job::RebuildSearchIndex { post_ids: vec![post_id], })] } Event::UserRegistered(data) => { vec![NewJob::from(Job::SendWelcomeEmail { - user_id: data.id, + user_id: *data.id, email: String::new(), username: data.username.clone(), })] } Event::MediaUploaded(data) => { vec![NewJob::from(Job::GenerateThumbnail { - media_id: data.id, + media_id: *data.id, size: 300, })] } Event::PasswordResetRequested { user, token } => { vec![NewJob::from(Job::SendPasswordResetEmail { - user_id: user.id, + user_id: *user.id, email: String::new(), reset_token: token.token.clone(), })] @@ -124,7 +124,7 @@ mod tests { excerpt: None, cover_image: None, status: PostStatus::Published, - created_by: 0, + created_by: None, author_name: None, category_id: None, category_name: None, @@ -152,8 +152,7 @@ mod tests { fn make_user(id: i64, username: &str) -> User { User { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, username: username.into(), role: UserRole::Reader, @@ -174,10 +173,9 @@ mod tests { fn make_media(id: i64, filename: &str) -> Media { Media { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, - user_id: 1, + user_id: crate::utils::id::SnowflakeId(1), filename: filename.into(), filepath: String::new(), mimetype: String::new(), @@ -195,10 +193,9 @@ mod tests { fn make_comment(id: i64) -> Comment { Comment { - id, - document_id: format!("doc-{id}"), + id: crate::utils::id::SnowflakeId(id), tenant_id: None, - post_id: 0, + post_id: crate::utils::id::SnowflakeId(0), created_by: None, updated_by: None, nickname: None, diff --git a/src/worker/handlers/payment_expire.rs b/src/worker/handlers/payment_expire.rs index 35b29e42..43d2b713 100644 --- a/src/worker/handlers/payment_expire.rs +++ b/src/worker/handlers/payment_expire.rs @@ -47,7 +47,7 @@ impl JobHandler for ExpirePaymentOrdersHandler { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Expired, Some("expired_at"), PaymentStatus::Pending, @@ -58,7 +58,7 @@ impl JobHandler for ExpirePaymentOrdersHandler { } else { tracing::info!( "[expire_payment_orders] order {} CAS failed, skipped", - order.document_id + order.id.to_string() ); skipped += 1; } @@ -71,7 +71,7 @@ impl JobHandler for ExpirePaymentOrdersHandler { Err(e) => { tracing::warn!( "[expire_payment_orders] error checking order {}: {e}", - order.document_id + order.id.to_string() ); skipped += 1; } @@ -101,13 +101,13 @@ impl ExpirePaymentOrdersHandler { tracing::warn!( "[expire_payment_orders] provider '{}' not available, skipping order {}", order.provider, - order.document_id + order.id.to_string() ); return Ok(false); } }; - let channel = payment_channel::find_by_id(&self.pool, order.channel_id, None) + let channel = payment_channel::find_by_id(&self.pool, *order.channel_id, None) .await? .ok_or_else(|| { crate::errors::app_error::AppError::Internal(anyhow::anyhow!( @@ -123,7 +123,7 @@ impl ExpirePaymentOrdersHandler { PaymentStatus::Paid => { tracing::info!( "[expire_payment_orders] order {} is paid at provider, skipping expiry", - order.document_id + order.id.to_string() ); Ok(false) } diff --git a/src/worker/handlers/payment_reconcile.rs b/src/worker/handlers/payment_reconcile.rs index 5f996f5e..62777cd2 100644 --- a/src/worker/handlers/payment_reconcile.rs +++ b/src/worker/handlers/payment_reconcile.rs @@ -54,7 +54,7 @@ impl JobHandler for ReconcilePaymentsHandler { Err(e) => { tracing::warn!( "[reconcile_payments] error reconciling order {}: {e}", - order.document_id + order.id.to_string() ); } } @@ -83,7 +83,7 @@ impl ReconcilePaymentsHandler { Err(_) => return Ok(true), }; - let channel = payment_channel::find_by_id(&self.pool, order.channel_id, None) + let channel = payment_channel::find_by_id(&self.pool, *order.channel_id, None) .await? .ok_or_else(|| { crate::errors::app_error::AppError::Internal(anyhow::anyhow!( @@ -98,7 +98,7 @@ impl ReconcilePaymentsHandler { if status.status != PaymentStatus::Paid { let detail = format!( "local=paid provider={:?} order={} amount={}", - status.status, order.document_id, order.amount + status.status, order.id, order.amount ); tracing::error!("[reconcile_payments] critical: {detail}"); if let Err(audit_err) = audit @@ -108,7 +108,7 @@ impl ReconcilePaymentsHandler { None, "payment_reconcile_mismatch", "payment_order", - Some(&order.document_id), + Some(&order.id.to_string()), Some(&detail), None, None, @@ -125,7 +125,7 @@ impl ReconcilePaymentsHandler { { let detail = format!( "amount_mismatch local={} provider={} order={}", - order.amount, provider_amount, order.document_id + order.amount, provider_amount, order.id ); tracing::error!("[reconcile_payments] critical: {detail}"); if let Err(audit_err) = audit @@ -135,7 +135,7 @@ impl ReconcilePaymentsHandler { None, "payment_reconcile_amount_mismatch", "payment_order", - Some(&order.document_id), + Some(&order.id.to_string()), Some(&detail), None, None, diff --git a/src/worker/handlers/payment_retry.rs b/src/worker/handlers/payment_retry.rs index bd76c26d..fc98bebb 100644 --- a/src/worker/handlers/payment_retry.rs +++ b/src/worker/handlers/payment_retry.rs @@ -40,7 +40,7 @@ impl JobHandler for RetryPaymentCallbackHandler { if order.status != PaymentStatus::Pending { tracing::info!( "[retry_payment_callback] order {} status is {:?}, skipping", - order.document_id, + order.id.to_string(), order.status ); return Ok(()); @@ -49,12 +49,12 @@ impl JobHandler for RetryPaymentCallbackHandler { let Some(ref provider_order_id) = order.provider_order_id else { tracing::warn!( "[retry_payment_callback] order {} has no provider_order_id, expiring", - order.document_id + order.id.to_string() ); crate::in_transaction!(&self.pool, tx, { crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Expired, Some("expired_at"), PaymentStatus::Pending, @@ -72,13 +72,13 @@ impl JobHandler for RetryPaymentCallbackHandler { tracing::warn!( "[retry_payment_callback] provider '{}' not available for order {}: {e}", order.provider, - order.document_id + order.id.to_string() ); return Ok(()); } }; - let channel = payment_channel::find_by_id(&self.pool, order.channel_id, None) + let channel = payment_channel::find_by_id(&self.pool, *order.channel_id, None) .await? .ok_or_else(|| { crate::errors::app_error::AppError::Internal(anyhow::anyhow!( @@ -93,12 +93,12 @@ impl JobHandler for RetryPaymentCallbackHandler { PaymentStatus::Paid => { tracing::info!( "[retry_payment_callback] order {} confirmed paid via provider query", - order.document_id + order.id.to_string() ); crate::in_transaction!(&self.pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Paid, Some("paid_at"), PaymentStatus::Pending, @@ -107,20 +107,20 @@ impl JobHandler for RetryPaymentCallbackHandler { if rows == 0 { tracing::info!( "[retry_payment_callback] order {} CAS failed, skipping", - order.document_id + order.id.to_string() ); return Ok(()); } let outbox_cmd = CreateWalletOutboxCmd { - user_id: order.user_id, + user_id: *order.user_id, currency: order.currency.clone(), amount: order.amount, entry_type: "credit".into(), tx_type: WalletTxType::Recharge, - transaction_no: format!("PAY-{}", order.document_id), + transaction_no: format!("PAY-{}", order.id), reference_type: Some(WalletReferenceType::Payment), - reference_id: Some(order.document_id.clone()), + reference_id: Some(order.id.to_string()), metadata: None, }; crate::models::wallet_outbox::tx_insert( @@ -137,7 +137,7 @@ impl JobHandler for RetryPaymentCallbackHandler { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Cancelled, Some("cancelled_at"), PaymentStatus::Pending, @@ -146,7 +146,7 @@ impl JobHandler for RetryPaymentCallbackHandler { if rows == 0 { tracing::info!( "[retry_payment_callback] order {} CAS failed for cancel", - order.document_id + order.id.to_string() ); } Ok(()) @@ -156,7 +156,7 @@ impl JobHandler for RetryPaymentCallbackHandler { crate::in_transaction!(&self.pool, tx, { let rows = crate::models::payment_order::tx_update_status_cas( &mut tx, - order.id, + *order.id, PaymentStatus::Expired, Some("expired_at"), PaymentStatus::Pending, @@ -165,7 +165,7 @@ impl JobHandler for RetryPaymentCallbackHandler { if rows == 0 { tracing::info!( "[retry_payment_callback] order {} CAS failed for expire", - order.document_id + order.id.to_string() ); } Ok(()) @@ -174,13 +174,13 @@ impl JobHandler for RetryPaymentCallbackHandler { PaymentStatus::Pending => { tracing::info!( "[retry_payment_callback] order {} still pending at provider, will retry later", - order.document_id + order.id.to_string() ); } _ => { tracing::info!( "[retry_payment_callback] order {} provider status {:?}, no action", - order.document_id, + order.id.to_string(), status.status ); } diff --git a/src/worker/handlers/publish.rs b/src/worker/handlers/publish.rs index 23c5447b..7623f98a 100644 --- a/src/worker/handlers/publish.rs +++ b/src/worker/handlers/publish.rs @@ -42,7 +42,7 @@ impl JobHandler for ScheduledPublishHandler { crate::models::post::update( &self.pool, &crate::commands::UpdatePostCmd { - id: post.id, + id: *post.id, title: None, slug: None, content: None, @@ -88,15 +88,10 @@ mod tests { ) .await .unwrap(); - user::update_role( - pool, - &u.document_id, - crate::models::user::UserRole::Author, - None, - ) - .await - .unwrap(); - u.id + user::update_role(pool, *u.id, crate::models::user::UserRole::Author, None) + .await + .unwrap(); + *u.id } #[tokio::test] @@ -124,10 +119,10 @@ mod tests { .unwrap(); let handler = ScheduledPublishHandler::new(pool.clone()); - let job = Job::ScheduledPublish { post_id: p.id }; + let job = Job::ScheduledPublish { post_id: *p.id }; assert!(handler.handle(&job).await.is_ok()); - let updated = post::find_by_id(&pool, p.id, None).await.unwrap().unwrap(); + let updated = post::find_by_id(&pool, *p.id, None).await.unwrap().unwrap(); assert_eq!(updated.status, PostStatus::Published); assert!(updated.published_at.is_some()); } @@ -157,7 +152,7 @@ mod tests { .unwrap(); let handler = ScheduledPublishHandler::new(pool); - let job = Job::ScheduledPublish { post_id: p.id }; + let job = Job::ScheduledPublish { post_id: *p.id }; assert!(handler.handle(&job).await.is_ok()); } diff --git a/src/worker/handlers/search_index.rs b/src/worker/handlers/search_index.rs index 54e1a90b..15f31cd0 100644 --- a/src/worker/handlers/search_index.rs +++ b/src/worker/handlers/search_index.rs @@ -47,7 +47,7 @@ impl JobHandler for RebuildSearchIndexHandler { for id in post_ids { match crate::models::post::find_by_id(&self.pool, *id, None).await { Ok(Some(post)) => posts.push(SearchablePost { - id: post.document_id, + id: post.id.to_string(), title: post.title, content: post.content, }), @@ -97,11 +97,11 @@ mod tests { #[cfg(feature = "search-tantivy")] async fn create_user(pool: &crate::db::Pool) -> i64 { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let (user_id,): (i64,) = sqlx::query_as( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email') RETURNING id", + "INSERT INTO users (id, username, role, status, registered_via) VALUES (?, 'testuser', 'author', 'active', 'email') RETURNING id", ) - .bind(&document_id) + .bind(id) .fetch_one(pool) .await .unwrap(); @@ -110,14 +110,14 @@ mod tests { #[cfg(feature = "search-tantivy")] async fn create_post(pool: &crate::db::Pool, author_id: i64, title: &str) -> (i64, String) { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_str(); let published = crate::models::post::PostStatus::Published.as_str(); let (int_id,): (i64,) = sqlx::query_as(&format!( - "INSERT INTO posts (document_id, title, slug, content, status, created_by, updated_by, view_count, is_pinned, created_at, updated_at) \ + "INSERT INTO posts (id, title, slug, content, status, created_by, updated_by, view_count, is_pinned, created_at, updated_at) \ VALUES (?, ?, ?, ?, '{published}', ?, NULL, 0, 0, ?, ?) RETURNING id", )) - .bind(&document_id) + .bind(id) .bind(title) .bind(title.to_lowercase().replace(' ', "-")) .bind(format!("{title} content")) @@ -127,7 +127,7 @@ mod tests { .fetch_one(pool) .await .unwrap(); - (int_id, document_id) + (int_id, id.to_string()) } #[cfg(feature = "search-tantivy")] @@ -135,7 +135,7 @@ mod tests { async fn indexes_existing_post_with_tantivy() { let pool = setup_pool().await; let uid = create_user(&pool).await; - let (post_int_id, post_doc_id) = create_post(&pool, uid, "Rust Programming Intro").await; + let (post_int_id, post_id_str) = create_post(&pool, uid, "Rust Programming Intro").await; let engine = Arc::new(crate::search::TantivyEngine::open_in_memory().unwrap()); let handler = RebuildSearchIndexHandler::new(pool, engine.clone()); @@ -146,7 +146,7 @@ mod tests { let (results, total) = engine.search("Rust", 1, 10).await.unwrap(); assert_eq!(total, 1); - assert_eq!(results[0].post_id, post_doc_id); + assert_eq!(results[0].post_id, post_id_str); } #[cfg(feature = "search-tantivy")] @@ -210,7 +210,7 @@ mod tests { async fn handles_mixed_existing_and_missing_posts() { let pool = setup_pool().await; let uid = create_user(&pool).await; - let (real_id, real_doc_id) = create_post(&pool, uid, "Real post").await; + let (real_id, real_id_str) = create_post(&pool, uid, "Real post").await; let engine = Arc::new(crate::search::TantivyEngine::open_in_memory().unwrap()); let handler = RebuildSearchIndexHandler::new(pool, engine.clone()); @@ -221,6 +221,6 @@ mod tests { let (results, total) = engine.search("Real", 1, 10).await.unwrap(); assert_eq!(total, 1); - assert_eq!(results[0].post_id, real_doc_id); + assert_eq!(results[0].post_id, real_id_str); } } diff --git a/src/worker/handlers/sitemap.rs b/src/worker/handlers/sitemap.rs index 56493e02..1985f0cd 100644 --- a/src/worker/handlers/sitemap.rs +++ b/src/worker/handlers/sitemap.rs @@ -103,8 +103,7 @@ mod tests { fn build_xml_with_posts() { use crate::models::post::Post; let posts = vec![Post { - id: 1i64, - document_id: "test-doc-id".to_string(), + id: crate::utils::id::SnowflakeId(1i64), tenant_id: Some(crate::constants::DEFAULT_TENANT.to_string()), title: "Hello".into(), slug: "hello".into(), @@ -112,8 +111,8 @@ mod tests { excerpt: None, cover_image: None, status: crate::models::post::PostStatus::Published, - created_by: 1i64, - updated_by: Some(1i64), + created_by: crate::utils::id::SnowflakeId(1i64), + updated_by: Some(crate::utils::id::SnowflakeId(1i64)), category_id: None, view_count: 0, is_pinned: false, diff --git a/src/worker/handlers/wallet_outbox.rs b/src/worker/handlers/wallet_outbox.rs index a1dafb82..9e77c53b 100644 --- a/src/worker/handlers/wallet_outbox.rs +++ b/src/worker/handlers/wallet_outbox.rs @@ -39,10 +39,10 @@ impl JobHandler for ProcessWalletOutboxHandler { let mut failed = 0u64; for entry in &entries { - if let Err(e) = wallet_outbox::mark_processing(&self.pool, entry.id).await { + if let Err(e) = wallet_outbox::mark_processing(&self.pool, *entry.id).await { tracing::warn!( "[wallet_outbox] CAS failed for entry {}, skipping: {e}", - entry.document_id + entry.id.to_string() ); continue; } @@ -51,7 +51,7 @@ impl JobHandler for ProcessWalletOutboxHandler { "credit" => { crate::services::wallet::credit_wallet( &self.pool, - entry.user_id, + *entry.user_id, &entry.currency, entry.amount, parse_tx_type(&entry.tx_type), @@ -68,7 +68,7 @@ impl JobHandler for ProcessWalletOutboxHandler { "debit" => { crate::services::wallet::debit_wallet( &self.pool, - entry.user_id, + *entry.user_id, &entry.currency, entry.amount, parse_tx_type(&entry.tx_type), @@ -89,16 +89,16 @@ impl JobHandler for ProcessWalletOutboxHandler { match result { Ok(_) => { - wallet_outbox::mark_completed(&self.pool, entry.id).await?; + wallet_outbox::mark_completed(&self.pool, *entry.id).await?; processed += 1; } Err(e) => { let err_str = format!("{e}"); tracing::warn!( "[wallet_outbox] failed to process entry {}: {err_str}", - entry.document_id + entry.id.to_string() ); - wallet_outbox::mark_failed(&self.pool, entry.id, &err_str).await?; + wallet_outbox::mark_failed(&self.pool, *entry.id, &err_str).await?; failed += 1; } } diff --git a/src/worker/runner.rs b/src/worker/runner.rs index 164142eb..fe82b1b0 100644 --- a/src/worker/runner.rs +++ b/src/worker/runner.rs @@ -61,10 +61,7 @@ impl WorkerRunner { Ok(jobs) => { for job in &jobs { if let Err(e) = self.execute(job).await { - tracing::error!( - "worker-{worker_id} job {} error: {e}", - job.document_id - ); + tracing::error!("worker-{worker_id} job {} error: {e}", job.id); } } } @@ -81,7 +78,7 @@ impl WorkerRunner { tracing::debug!( "executing job {} type={} attempt={}/{}", - job.document_id, + job.id, job_type, job.attempts, job.max_attempts, @@ -94,20 +91,18 @@ impl WorkerRunner { dispatcher.dispatch(&job.job).await } else { tracing::warn!("no handler for job type '{job_type}', marking dead"); - self.queue - .dead(&job.document_id, "no handler registered") - .await?; + self.queue.dead(&job.id, "no handler registered").await?; return Ok(()); }; match result { - Ok(()) => self.queue.complete(&job.document_id).await, + Ok(()) => self.queue.complete(&job.id).await, Err(e) => { let err_msg = format!("{e}"); if job.attempts >= job.max_attempts { - self.queue.dead(&job.document_id, &err_msg).await + self.queue.dead(&job.id, &err_msg).await } else { - self.queue.fail(&job.document_id, &err_msg).await + self.queue.fail(&job.id, &err_msg).await } } } diff --git a/src/worker/scheduler.rs b/src/worker/scheduler.rs index 3e3b62f9..eb7d5e88 100644 --- a/src/worker/scheduler.rs +++ b/src/worker/scheduler.rs @@ -40,6 +40,7 @@ use crate::db::Pool; use crate::db::dialect::ph; use crate::errors::app_error::{AppError, AppResult}; use crate::plugins::CronEntry; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; use super::{CronExecStatus, Job, JobQueue, NewJob}; @@ -48,8 +49,7 @@ macro_rules! cron_row_to_schedule { ($r:expr) => {{ let r = $r; CronSchedule { - id: r.id, - document_id: r.document_id, + id: SnowflakeId(r.id), label: r.label, job_type: r.job_type, payload: r.payload, @@ -68,9 +68,8 @@ macro_rules! exec_log_row_to_struct { ($r:expr) => {{ let r = $r; CronExecutionLog { - id: r.id, - document_id: r.document_id, - schedule_id: r.schedule_id, + id: SnowflakeId(r.id), + schedule_id: SnowflakeId(r.schedule_id), job_type: r.job_type, label: r.label, status: r.status, @@ -85,7 +84,6 @@ macro_rules! exec_log_row_to_struct { #[derive(sqlx::FromRow)] struct CronScheduleRow { id: i64, - document_id: String, label: String, job_type: String, payload: Option, @@ -101,7 +99,6 @@ struct CronScheduleRow { #[derive(sqlx::FromRow)] struct CronExecLogRow { id: i64, - document_id: String, schedule_id: i64, job_type: String, label: String, @@ -122,8 +119,7 @@ struct PluginCronRow { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, serde::Serialize)] pub struct CronSchedule { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub label: String, pub job_type: String, pub payload: Option, @@ -176,17 +172,15 @@ pub async fn create_schedule_with_plugin( enabled: bool, plugin_id: Option<&str>, ) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); let now = crate::utils::tz::now_utc(); let next = next_run(cron_expr, now)?; let (int_id,): (i64,) = sqlx::query_as(&format!( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) - VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}) + "INSERT INTO cron_schedules (label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}) RETURNING id", - ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8), ph(9), ph(10) + ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8), ph(9) )) - .bind(&document_id) .bind(label) .bind(job_type) .bind(payload) @@ -200,8 +194,7 @@ pub async fn create_schedule_with_plugin( .await?; Ok(CronSchedule { - id: int_id, - document_id, + id: SnowflakeId(int_id), label: label.to_string(), job_type: job_type.to_string(), payload: payload.map(|s| s.to_string()), @@ -216,10 +209,10 @@ pub async fn create_schedule_with_plugin( } /// Find by ID -pub async fn find_by_id(pool: &Pool, id: &str) -> AppResult> { +pub async fn find_by_id(pool: &Pool, id: i64) -> AppResult> { let row = sqlx::query_as::<_, CronScheduleRow>(&format!( - "SELECT id, document_id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at - FROM cron_schedules WHERE document_id = {}", + "SELECT id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at + FROM cron_schedules WHERE id = {}", ph(1) )) .bind(id) @@ -232,7 +225,7 @@ pub async fn find_by_id(pool: &Pool, id: &str) -> AppResult /// List all schedules pub async fn list_schedules(pool: &Pool) -> AppResult> { let rows = sqlx::query_as::<_, CronScheduleRow>( - "SELECT id, document_id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at + "SELECT id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at FROM cron_schedules ORDER BY created_at ASC", ) .fetch_all(pool) @@ -242,10 +235,10 @@ pub async fn list_schedules(pool: &Pool) -> AppResult> { } /// Enable/disable a schedule -pub async fn toggle_schedule(pool: &Pool, id: &str, enabled: bool) -> AppResult<()> { +pub async fn toggle_schedule(pool: &Pool, id: i64, enabled: bool) -> AppResult<()> { let now = crate::utils::tz::now_utc(); let result = sqlx::query(&format!( - "UPDATE cron_schedules SET enabled = {}, updated_at = {} WHERE document_id = {}", + "UPDATE cron_schedules SET enabled = {}, updated_at = {} WHERE id = {}", ph(1), ph(2), ph(3) @@ -267,7 +260,7 @@ pub async fn toggle_schedule(pool: &Pool, id: &str, enabled: bool) -> AppResult< /// Merges provided fields, recalculates `next_run_at`, persists and returns the updated schedule. pub async fn update_schedule( pool: &Pool, - id: &str, + id: i64, label: Option, job_type: Option, payload: Option>, @@ -298,7 +291,7 @@ pub async fn update_schedule( let now = crate::utils::tz::now_utc(); sqlx::query(&format!( - "UPDATE cron_schedules SET label = {}, job_type = {}, payload = {}, cron_expr = {}, enabled = {}, next_run_at = {}, updated_at = {} WHERE document_id = {}", + "UPDATE cron_schedules SET label = {}, job_type = {}, payload = {}, cron_expr = {}, enabled = {}, next_run_at = {}, updated_at = {} WHERE id = {}", ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8) )) .bind(&schedule.label) @@ -316,14 +309,11 @@ pub async fn update_schedule( } /// Delete a schedule -pub async fn delete_schedule(pool: &Pool, id: &str) -> AppResult<()> { - let result = sqlx::query(&format!( - "DELETE FROM cron_schedules WHERE document_id = {}", - ph(1) - )) - .bind(id) - .execute(pool) - .await?; +pub async fn delete_schedule(pool: &Pool, id: i64) -> AppResult<()> { + let result = sqlx::query(&format!("DELETE FROM cron_schedules WHERE id = {}", ph(1))) + .bind(id) + .execute(pool) + .await?; if result.rows_affected() == 0 { return Err(AppError::not_found("cron_schedule")); @@ -374,7 +364,7 @@ impl CronScheduler { let now = crate::utils::tz::now_utc(); let rows = sqlx::query_as::<_, CronScheduleRow>(&format!( - "SELECT id, document_id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at + "SELECT id, label, job_type, payload, cron_expr, enabled, last_run_at, next_run_at, plugin_id, created_at, updated_at FROM cron_schedules WHERE enabled = 1 AND next_run_at <= {}", ph(1) )) @@ -404,10 +394,14 @@ impl CronScheduler { schedule.job_type ); - let log_id = - create_execution_log(&self.pool, schedule.id, &schedule.job_type, &schedule.label) - .await - .ok(); + let log_id = create_execution_log( + &self.pool, + *schedule.id, + &schedule.job_type, + &schedule.label, + ) + .await + .ok(); let start = std::time::Instant::now(); @@ -428,7 +422,7 @@ impl CronScheduler { Ok(()) => { if let Some(ref lid) = log_id { sqlx::query(&format!( - "UPDATE cron_execution_log SET status = {}, duration_ms = {}, finished_at = {} WHERE document_id = {}", + "UPDATE cron_execution_log SET status = {}, duration_ms = {}, finished_at = {} WHERE id = {}", ph(1), ph(2), ph(3), ph(4) )) .bind(CronExecStatus::Completed) @@ -443,7 +437,7 @@ impl CronScheduler { if let Some(ref lid) = log_id { let err_str = e.to_string(); sqlx::query(&format!( - "UPDATE cron_execution_log SET status = {}, duration_ms = {}, error = {}, finished_at = {} WHERE document_id = {}", + "UPDATE cron_execution_log SET status = {}, duration_ms = {}, error = {}, finished_at = {} WHERE id = {}", ph(1), ph(2), ph(3), ph(4), ph(5) )) .bind(CronExecStatus::Failed) @@ -547,9 +541,8 @@ pub async fn seed_defaults( #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, serde::Serialize)] pub struct CronExecutionLog { - pub id: i64, - pub document_id: String, - pub schedule_id: i64, + pub id: SnowflakeId, + pub schedule_id: SnowflakeId, pub job_type: String, pub label: String, pub status: CronExecStatus, @@ -565,12 +558,12 @@ pub async fn create_execution_log( schedule_id: i64, job_type: &str, label: &str, -) -> AppResult { - let document_id = uuid::Uuid::now_v7().to_string(); +) -> AppResult { + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); sqlx::query(&format!( - "INSERT INTO cron_execution_log (document_id, schedule_id, job_type, label, status, started_at) + "INSERT INTO cron_execution_log (id, schedule_id, job_type, label, status, started_at) VALUES ({}, {}, {}, {}, {}, {})", ph(1), ph(2), @@ -579,7 +572,7 @@ pub async fn create_execution_log( ph(5), ph(6) )) - .bind(&document_id) + .bind(id) .bind(schedule_id) .bind(job_type) .bind(label) @@ -588,14 +581,14 @@ pub async fn create_execution_log( .execute(pool) .await?; - Ok(document_id) + Ok(id) } /// Mark execution log as succeeded -pub async fn complete_execution_log(pool: &Pool, log_id: &str, duration_ms: i64) -> AppResult<()> { +pub async fn complete_execution_log(pool: &Pool, log_id: i64, duration_ms: i64) -> AppResult<()> { let now = crate::utils::tz::now_utc(); sqlx::query(&format!( - "UPDATE cron_execution_log SET status = {}, duration_ms = {}, finished_at = {} WHERE document_id = {}", + "UPDATE cron_execution_log SET status = {}, duration_ms = {}, finished_at = {} WHERE id = {}", ph(1), ph(2), ph(3), ph(4) )) .bind(CronExecStatus::Completed) @@ -610,13 +603,13 @@ pub async fn complete_execution_log(pool: &Pool, log_id: &str, duration_ms: i64) /// Mark execution log as failed pub async fn fail_execution_log( pool: &Pool, - log_id: &str, + log_id: i64, duration_ms: i64, error: &str, ) -> AppResult<()> { let now = crate::utils::tz::now_utc(); sqlx::query(&format!( - "UPDATE cron_execution_log SET status = {}, duration_ms = {}, error = {}, finished_at = {} WHERE document_id = {}", + "UPDATE cron_execution_log SET status = {}, duration_ms = {}, error = {}, finished_at = {} WHERE id = {}", ph(1), ph(2), ph(3), ph(4), ph(5) )) .bind(CronExecStatus::Failed) @@ -632,18 +625,17 @@ pub async fn fail_execution_log( /// Query execution history for a schedule pub async fn list_execution_logs( pool: &Pool, - schedule_document_id: &str, + schedule_id: i64, limit: i64, ) -> AppResult> { let rows = sqlx::query_as::<_, CronExecLogRow>(&format!( - "SELECT el.id, el.document_id, el.schedule_id, el.job_type, el.label, el.status, el.duration_ms, el.error, el.started_at, el.finished_at + "SELECT el.id, el.schedule_id, el.job_type, el.label, el.status, el.duration_ms, el.error, el.started_at, el.finished_at FROM cron_execution_log el - JOIN cron_schedules cs ON el.schedule_id = cs.id - WHERE cs.document_id = {} + WHERE el.schedule_id = {} ORDER BY el.started_at DESC LIMIT {}", ph(1), ph(2) )) - .bind(schedule_document_id) + .bind(schedule_id) .bind(limit) .fetch_all(pool) .await?; @@ -657,7 +649,7 @@ pub async fn list_execution_logs( /// Query recent execution records across all schedules pub async fn recent_execution_logs(pool: &Pool, limit: i64) -> AppResult> { let rows = sqlx::query_as::<_, CronExecLogRow>(&format!( - "SELECT id, document_id, schedule_id, job_type, label, status, duration_ms, error, started_at, finished_at + "SELECT id, schedule_id, job_type, label, status, duration_ms, error, started_at, finished_at FROM cron_execution_log ORDER BY started_at DESC LIMIT {}", ph(1) @@ -756,15 +748,15 @@ pub async fn sync_plugin_crons( tracing::debug!("updated cron '{}' for plugin {plugin_id}", entry.job_type); } else { - let document_id = uuid::Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); let next = next_run(&entry.cron_expr, crate::utils::tz::now_utc())?; sqlx::query(&format!( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + "INSERT INTO cron_schedules (id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8), ph(9), ph(10) )) - .bind(&document_id) + .bind(id) .bind(&entry.label) .bind(&entry.job_type) .bind(&entry.payload) @@ -851,7 +843,7 @@ mod tests { assert!(s.enabled); assert!(s.next_run_at.to_rfc3339().len() > 10); - let found = find_by_id(&pool, &s.document_id).await.unwrap().unwrap(); + let found = find_by_id(&pool, *s.id).await.unwrap().unwrap(); assert_eq!(found.job_type, "generate_sitemap"); } @@ -874,12 +866,12 @@ mod tests { .await .unwrap(); - toggle_schedule(&pool, &s.document_id, false).await.unwrap(); - let found = find_by_id(&pool, &s.document_id).await.unwrap().unwrap(); + toggle_schedule(&pool, *s.id, false).await.unwrap(); + let found = find_by_id(&pool, *s.id).await.unwrap().unwrap(); assert!(!found.enabled); - delete_schedule(&pool, &s.document_id).await.unwrap(); - assert!(find_by_id(&pool, &s.document_id).await.unwrap().is_none()); + delete_schedule(&pool, *s.id).await.unwrap(); + assert!(find_by_id(&pool, *s.id).await.unwrap().is_none()); } #[tokio::test] @@ -890,7 +882,7 @@ mod tests { .await .unwrap(); - let result = toggle_schedule(&pool, "nonexistent", true).await; + let result = toggle_schedule(&pool, 9999999, true).await; assert!(result.is_err()); } @@ -947,10 +939,10 @@ mod tests { let past = (now - chrono::Duration::hours(1)).to_rfc3339(); sqlx::query( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + "INSERT INTO cron_schedules (id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, ?, NULL, ?, ?)", ) - .bind("sched-1") + .bind(1i64) .bind("Test Sitemap") .bind("generate_sitemap") .bind(Option::::None) @@ -965,18 +957,17 @@ mod tests { let scheduler = CronScheduler::new(pool.clone(), queue, std::time::Duration::from_secs(60)); scheduler.tick().await.unwrap(); - let jobs = sqlx::query_as::<_, (String, String)>("SELECT document_id, job_type FROM jobs") + let jobs = sqlx::query_as::<_, (String, String)>("SELECT job_type, status FROM jobs") .fetch_all(&pool) .await .unwrap(); assert_eq!(jobs.len(), 1); - assert_eq!(jobs[0].1, "generate_sitemap"); + assert_eq!(jobs[0].0, "generate_sitemap"); - let row: (String,) = - sqlx::query_as("SELECT next_run_at FROM cron_schedules WHERE document_id = 'sched-1'") - .fetch_one(&pool) - .await - .unwrap(); + let row: (String,) = sqlx::query_as("SELECT next_run_at FROM cron_schedules WHERE id = 1") + .fetch_one(&pool) + .await + .unwrap(); assert_ne!(row.0, past); } @@ -994,10 +985,10 @@ mod tests { let future = (now + chrono::Duration::hours(1)).to_rfc3339(); sqlx::query( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + "INSERT INTO cron_schedules (id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, ?, NULL, ?, ?)", ) - .bind("sched-future") + .bind(2i64) .bind("Future Job") .bind("generate_sitemap") .bind(Option::::None) @@ -1192,47 +1183,42 @@ mod tests { pool } - async fn insert_test_schedule(pool: &Pool, document_id: &str) -> i64 { + async fn insert_test_schedule(pool: &Pool) -> i64 { + let id = crate::utils::id::new_id(); let now = Utc::now(); sqlx::query( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + "INSERT INTO cron_schedules (id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) VALUES (?, 'Test', 'test_task', NULL, '0 */5 * * * *', 1, ?, NULL, ?, ?)", ) - .bind(document_id) + .bind(id) .bind(now.to_rfc3339()) .bind(now.to_rfc3339()) .bind(now.to_rfc3339()) .execute(pool) .await .unwrap(); - - let (id,): (i64,) = sqlx::query_as("SELECT id FROM cron_schedules WHERE document_id = ?") - .bind(document_id) - .fetch_one(pool) - .await - .unwrap(); id } #[tokio::test] async fn execution_log_create_and_complete() { let pool = setup_log_tables().await; - let sched_id = insert_test_schedule(&pool, "sched-1").await; + let sched_id = insert_test_schedule(&pool).await; let log_id = create_execution_log(&pool, sched_id, "generate_sitemap", "Sitemap") .await .unwrap(); - let logs = list_execution_logs(&pool, "sched-1", 10).await.unwrap(); + let logs = list_execution_logs(&pool, sched_id, 10).await.unwrap(); assert_eq!(logs.len(), 1); assert_eq!(logs[0].status, CronExecStatus::Running); assert_eq!(logs[0].job_type, "generate_sitemap"); assert!(logs[0].duration_ms.is_none()); assert!(logs[0].finished_at.is_none()); - complete_execution_log(&pool, &log_id, 42).await.unwrap(); + complete_execution_log(&pool, log_id, 42).await.unwrap(); - let logs = list_execution_logs(&pool, "sched-1", 10).await.unwrap(); + let logs = list_execution_logs(&pool, sched_id, 10).await.unwrap(); assert_eq!(logs[0].status, CronExecStatus::Completed); assert_eq!(logs[0].duration_ms, Some(42)); assert!(logs[0].finished_at.is_some()); @@ -1241,17 +1227,17 @@ mod tests { #[tokio::test] async fn execution_log_fail_records_error() { let pool = setup_log_tables().await; - let sched_id = insert_test_schedule(&pool, "sched-1").await; + let sched_id = insert_test_schedule(&pool).await; let log_id = create_execution_log(&pool, sched_id, "my_task", "Task") .await .unwrap(); - fail_execution_log(&pool, &log_id, 100, "something broke") + fail_execution_log(&pool, log_id, 100, "something broke") .await .unwrap(); - let logs = list_execution_logs(&pool, "sched-1", 10).await.unwrap(); + let logs = list_execution_logs(&pool, sched_id, 10).await.unwrap(); assert_eq!(logs[0].status, CronExecStatus::Failed); assert_eq!(logs[0].duration_ms, Some(100)); assert_eq!(logs[0].error, Some("something broke".into())); @@ -1260,8 +1246,8 @@ mod tests { #[tokio::test] async fn execution_log_list_by_schedule() { let pool = setup_log_tables().await; - let sched_a = insert_test_schedule(&pool, "sched-a").await; - let sched_b = insert_test_schedule(&pool, "sched-b").await; + let sched_a = insert_test_schedule(&pool).await; + let sched_b = insert_test_schedule(&pool).await; create_execution_log(&pool, sched_a, "task_a", "A") .await @@ -1273,18 +1259,18 @@ mod tests { .await .unwrap(); - let a = list_execution_logs(&pool, "sched-a", 10).await.unwrap(); + let a = list_execution_logs(&pool, sched_a, 10).await.unwrap(); assert_eq!(a.len(), 2); - let b = list_execution_logs(&pool, "sched-b", 10).await.unwrap(); + let b = list_execution_logs(&pool, sched_b, 10).await.unwrap(); assert_eq!(b.len(), 1); } #[tokio::test] async fn execution_log_recent_ordering() { let pool = setup_log_tables().await; - let s1 = insert_test_schedule(&pool, "s1").await; - let s2 = insert_test_schedule(&pool, "s2").await; + let s1 = insert_test_schedule(&pool).await; + let s2 = insert_test_schedule(&pool).await; create_execution_log(&pool, s1, "task_1", "First") .await @@ -1301,7 +1287,7 @@ mod tests { #[tokio::test] async fn execution_log_cleanup_removes_old() { let pool = setup_log_tables().await; - let s1 = insert_test_schedule(&pool, "s1").await; + let s1 = insert_test_schedule(&pool).await; create_execution_log(&pool, s1, "task_1", "Old") .await @@ -1310,7 +1296,7 @@ mod tests { let count = cleanup_execution_logs(&pool, 0).await.unwrap(); assert_eq!(count, 1); - let logs = list_execution_logs(&pool, "s1", 10).await.unwrap(); + let logs = list_execution_logs(&pool, s1, 10).await.unwrap(); assert!(logs.is_empty()); } @@ -1329,10 +1315,10 @@ mod tests { let past = (now - chrono::Duration::hours(1)).to_rfc3339(); sqlx::query( - "INSERT INTO cron_schedules (document_id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) + "INSERT INTO cron_schedules (id, label, job_type, payload, cron_expr, enabled, next_run_at, plugin_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, ?, NULL, ?, ?)", ) - .bind("sched-log-test") + .bind(3i64) .bind("Log Test") .bind("generate_sitemap") .bind(Option::::None) @@ -1346,9 +1332,7 @@ mod tests { scheduler.tick().await.unwrap(); - let logs = list_execution_logs(&pool, "sched-log-test", 10) - .await - .unwrap(); + let logs = list_execution_logs(&pool, 3, 10).await.unwrap(); assert_eq!(logs.len(), 1); assert_eq!(logs[0].status, CronExecStatus::Completed); assert!(logs[0].duration_ms.is_some()); diff --git a/src/worker/sqlite_queue.rs b/src/worker/sqlite_queue.rs index 2051dcdd..b8cc6a94 100644 --- a/src/worker/sqlite_queue.rs +++ b/src/worker/sqlite_queue.rs @@ -1,9 +1,8 @@ //! `SQLite` persisted job queue use sqlx::Row; -use uuid::Uuid; -use crate::constants::{COL_DOCUMENT_ID, COL_ID}; +use crate::constants::COL_ID; use crate::db::Pool; use crate::db::dialect::ph; use crate::errors::app_error::{AppError, AppResult}; @@ -29,18 +28,18 @@ impl SqliteJobQueue { #[async_trait::async_trait] impl JobQueue for SqliteJobQueue { async fn enqueue(&self, new_job: NewJob) -> AppResult<()> { - let id = Uuid::now_v7().to_string(); + let id = crate::utils::id::new_id(); let now = crate::utils::tz::now_utc(); let job_type = new_job.job.job_type(); let payload = serialize_job(&new_job.job); let max_attempts = new_job.max_attempts.unwrap_or(3); sqlx::query(&format!( - "INSERT INTO jobs ({COL_DOCUMENT_ID}, job_type, payload, status, max_attempts, run_after, created_at, updated_at) + "INSERT INTO jobs ({COL_ID}, job_type, payload, status, max_attempts, run_after, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), ph(4), ph(5), ph(6), ph(7), ph(8) )) - .bind(&id) + .bind(id) .bind(job_type) .bind(&payload) .bind(JobStatus::Pending) @@ -60,7 +59,7 @@ impl JobQueue for SqliteJobQueue { let limit_i64 = limit as i64; let returning = crate::db::dialect::returning_col(&format!( - "{COL_DOCUMENT_ID} as {COL_ID}, job_type, payload, attempts, max_attempts, created_at" + "{COL_ID}, job_type, payload, attempts, max_attempts, created_at" )); let sql = format!( "UPDATE jobs SET status = {}, attempts = attempts + 1, updated_at = {} @@ -88,7 +87,7 @@ impl JobQueue for SqliteJobQueue { let mut jobs = Vec::with_capacity(rows.len()); for row in rows { - let id: String = row.get::, _>("id").unwrap_or_default(); + let id: i64 = row.get::, _>("id").unwrap_or_default(); let job_type: String = row.get("job_type"); let payload: String = row.get("payload"); let attempts: i32 = row.get("attempts"); @@ -96,7 +95,7 @@ impl JobQueue for SqliteJobQueue { let created_at: Timestamp = row.get("created_at"); match parse_job(&job_type, &payload) { Ok(job) => jobs.push(QueuedJob { - document_id: id, + id: id.to_string(), job, attempts: attempts as u32, max_attempts: max_attempts as u32, @@ -104,7 +103,9 @@ impl JobQueue for SqliteJobQueue { }), Err(e) => { tracing::error!("failed to parse job {id}: {e}"); - let _ = self.dead(&id, &format!("parse error: {e}")).await; + let _ = self + .dead(&id.to_string(), &format!("parse error: {e}")) + .await; } } } @@ -114,8 +115,11 @@ impl JobQueue for SqliteJobQueue { async fn complete(&self, id: &str) -> AppResult<()> { let now = crate::utils::tz::now_utc(); + let id: i64 = id + .parse() + .map_err(|e| AppError::Internal(anyhow::anyhow!("invalid id: {e}")))?; sqlx::query(&format!( - "UPDATE jobs SET status = {}, updated_at = {} WHERE {COL_DOCUMENT_ID} = {}", + "UPDATE jobs SET status = {}, updated_at = {} WHERE {COL_ID} = {}", ph(1), ph(2), ph(3) @@ -131,10 +135,13 @@ impl JobQueue for SqliteJobQueue { async fn fail(&self, id: &str, error: &str) -> AppResult<()> { let now = crate::utils::tz::now_utc(); + let id: i64 = id + .parse() + .map_err(|e| AppError::Internal(anyhow::anyhow!("invalid id: {e}")))?; in_transaction!(&self.pool, tx, { let row = sqlx::query(&format!( - "SELECT attempts, max_attempts FROM jobs WHERE {COL_DOCUMENT_ID} = {}", + "SELECT attempts, max_attempts FROM jobs WHERE {COL_ID} = {}", ph(1) )) .bind(id) @@ -150,7 +157,7 @@ impl JobQueue for SqliteJobQueue { if attempts >= max_attempts { sqlx::query(&format!( - "UPDATE jobs SET status = {}, error = {}, updated_at = {} WHERE {COL_DOCUMENT_ID} = {}", + "UPDATE jobs SET status = {}, error = {}, updated_at = {} WHERE {COL_ID} = {}", ph(1), ph(2), ph(3), @@ -171,7 +178,7 @@ impl JobQueue for SqliteJobQueue { crate::utils::tz::now_utc() + chrono::Duration::from_std(delay).unwrap_or_default(); sqlx::query(&format!( - "UPDATE jobs SET status = {}, error = {}, run_after = {}, updated_at = {} WHERE {COL_DOCUMENT_ID} = {}", + "UPDATE jobs SET status = {}, error = {}, run_after = {}, updated_at = {} WHERE {COL_ID} = {}", ph(1), ph(2), ph(3), ph(4), ph(5) )) .bind(JobStatus::Pending) @@ -191,8 +198,11 @@ impl JobQueue for SqliteJobQueue { async fn dead(&self, id: &str, error: &str) -> AppResult<()> { let now = crate::utils::tz::now_utc(); + let id: i64 = id + .parse() + .map_err(|e| AppError::Internal(anyhow::anyhow!("invalid id: {e}")))?; sqlx::query(&format!( - "UPDATE jobs SET status = {}, error = {}, updated_at = {} WHERE {COL_DOCUMENT_ID} = {}", + "UPDATE jobs SET status = {}, error = {}, updated_at = {} WHERE {COL_ID} = {}", ph(1), ph(2), ph(3), @@ -250,7 +260,7 @@ impl JobQueue for SqliteJobQueue { let (items, total): (Vec, i64) = if let Some(s) = status { let rows = sqlx::query(&format!( - "SELECT {COL_DOCUMENT_ID} as {COL_ID}, job_type, payload, status, attempts, max_attempts, run_after, error, created_at, updated_at + "SELECT {COL_ID}, job_type, payload, status, attempts, max_attempts, run_after, error, created_at, updated_at FROM jobs WHERE status = {} ORDER BY created_at DESC LIMIT {} OFFSET {}", ph(1), ph(2), ph(3) )) @@ -272,7 +282,10 @@ impl JobQueue for SqliteJobQueue { let items = rows .into_iter() .map(|r| JobRow { - document_id: r.get::, _>(COL_ID).unwrap_or_default(), + id: r + .get::, _>(COL_ID) + .map(|i| i.to_string()) + .unwrap_or_default(), job_type: r.get("job_type"), payload: r.get("payload"), status: r.get("status"), @@ -288,7 +301,7 @@ impl JobQueue for SqliteJobQueue { (items, total) } else { let rows = sqlx::query(&format!( - "SELECT {COL_DOCUMENT_ID} as {COL_ID}, job_type, payload, status, attempts, max_attempts, run_after, error, created_at, updated_at + "SELECT {COL_ID}, job_type, payload, status, attempts, max_attempts, run_after, error, created_at, updated_at FROM jobs ORDER BY created_at DESC LIMIT {} OFFSET {}", ph(1), ph(2) )) @@ -305,7 +318,10 @@ impl JobQueue for SqliteJobQueue { let items = rows .into_iter() .map(|r| JobRow { - document_id: r.get::, _>(COL_ID).unwrap_or_default(), + id: r + .get::, _>(COL_ID) + .map(|i| i.to_string()) + .unwrap_or_default(), job_type: r.get("job_type"), payload: r.get("payload"), status: r.get("status"), @@ -326,9 +342,12 @@ impl JobQueue for SqliteJobQueue { async fn retry(&self, id: &str) -> AppResult<()> { let now = crate::utils::tz::now_utc(); + let id: i64 = id + .parse() + .map_err(|e| AppError::Internal(anyhow::anyhow!("invalid id: {e}")))?; let result = sqlx::query(&format!( "UPDATE jobs SET status = {}, attempts = 0, error = NULL, run_after = NULL, updated_at = {} - WHERE {COL_DOCUMENT_ID} = {} AND status = {}", + WHERE {COL_ID} = {} AND status = {}", ph(1), ph(2), ph(3), @@ -350,13 +369,13 @@ impl JobQueue for SqliteJobQueue { } async fn remove(&self, id: &str) -> AppResult<()> { - let result = sqlx::query(&format!( - "DELETE FROM jobs WHERE {COL_DOCUMENT_ID} = {}", - ph(1) - )) - .bind(id) - .execute(&self.pool) - .await?; + let id: i64 = id + .parse() + .map_err(|e| AppError::Internal(anyhow::anyhow!("invalid id: {e}")))?; + let result = sqlx::query(&format!("DELETE FROM jobs WHERE {COL_ID} = {}", ph(1))) + .bind(id) + .execute(&self.pool) + .await?; if result.rows_affected() == 0 { return Err(AppError::not_found("job")); @@ -450,7 +469,7 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(10).await.unwrap(); - q.complete(&jobs[0].document_id).await.unwrap(); + q.complete(&jobs[0].id).await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.completed, 1); @@ -464,9 +483,7 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(10).await.unwrap(); - q.fail(&jobs[0].document_id, "something went wrong") - .await - .unwrap(); + q.fail(&jobs[0].id, "something went wrong").await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.pending, 1); @@ -492,9 +509,7 @@ mod tests { assert_eq!(jobs[0].attempts, 1); assert_eq!(jobs[0].max_attempts, 1); - q.fail(&jobs[0].document_id, "permanent failure") - .await - .unwrap(); + q.fail(&jobs[0].id, "permanent failure").await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.dead, 1); @@ -508,7 +523,7 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(10).await.unwrap(); - q.dead(&jobs[0].document_id, "fatal").await.unwrap(); + q.dead(&jobs[0].id, "fatal").await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.dead, 1); @@ -517,7 +532,7 @@ mod tests { #[tokio::test] async fn dead_returns_not_found_for_missing_job() { let q = setup().await; - let result = q.dead("nonexistent", "err").await; + let result = q.dead("99999999", "err").await; assert!(result.is_ok()); } @@ -530,7 +545,7 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(1).await.unwrap(); - q.complete(&jobs[0].document_id).await.unwrap(); + q.complete(&jobs[0].id).await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.pending, 2); @@ -562,7 +577,7 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(1).await.unwrap(); - q.complete(&jobs[0].document_id).await.unwrap(); + q.complete(&jobs[0].id).await.unwrap(); let (pending, _) = q.list(Some(JobStatus::Pending), 1, 10).await.unwrap(); assert_eq!(pending.len(), 1); @@ -601,12 +616,12 @@ mod tests { .unwrap(); let jobs = q.dequeue(10).await.unwrap(); - q.fail(&jobs[0].document_id, "err").await.unwrap(); + q.fail(&jobs[0].id, "err").await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.dead, 1); - q.retry(&jobs[0].document_id).await.unwrap(); + q.retry(&jobs[0].id).await.unwrap(); let stats = q.stats().await.unwrap(); assert_eq!(stats.pending, 1); @@ -624,14 +639,14 @@ mod tests { q.enqueue(sample_job()).await.unwrap(); let jobs = q.dequeue(10).await.unwrap(); - let result = q.retry(&jobs[0].document_id).await; + let result = q.retry(&jobs[0].id).await; assert!(result.is_err()); } #[tokio::test] async fn retry_nonexistent_returns_not_found() { let q = setup().await; - let result = q.retry("nonexistent").await; + let result = q.retry("99999999").await; assert!(result.is_err()); } @@ -643,7 +658,7 @@ mod tests { let (rows, _) = q.list(None, 1, 10).await.unwrap(); assert_eq!(rows.len(), 1); - q.remove(&rows[0].document_id).await.unwrap(); + q.remove(&rows[0].id).await.unwrap(); let (rows, _) = q.list(None, 1, 10).await.unwrap(); assert!(rows.is_empty()); @@ -652,7 +667,7 @@ mod tests { #[tokio::test] async fn remove_nonexistent_returns_not_found() { let q = setup().await; - let result = q.remove("nonexistent").await; + let result = q.remove("99999999").await; assert!(result.is_err()); } @@ -702,7 +717,7 @@ mod tests { #[tokio::test] async fn fail_on_nonexistent_returns_not_found() { let q = setup().await; - let result = q.fail("nonexistent", "err").await; + let result = q.fail("99999999", "err").await; assert!(result.is_err()); } } diff --git a/src/workflow/engine.rs b/src/workflow/engine.rs index 8bfc6723..58ed7d8f 100644 --- a/src/workflow/engine.rs +++ b/src/workflow/engine.rs @@ -37,7 +37,7 @@ impl WorkflowService { /// Create workflow definition pub async fn create_workflow( &self, - id: &str, + id: i64, name: &str, description: Option<&str>, steps: &[StepDef], @@ -66,7 +66,7 @@ impl WorkflowService { } /// Get workflow definition - pub async fn get_workflow(&self, id: &str) -> AppResult { + pub async fn get_workflow(&self, id: i64) -> AppResult { get_definition(&self.pool, id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? @@ -82,7 +82,7 @@ impl WorkflowService { async fn get_definition_by_pk(&self, id: i64) -> AppResult { let sql = format!( - "SELECT id, document_id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions WHERE id = {}", + "SELECT id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions WHERE id = {}", crate::db::dialect::ph(1) ); sqlx::query_as::<_, WorkflowDefinition>(&sql) @@ -94,7 +94,7 @@ impl WorkflowService { } /// Delete workflow definition - pub async fn delete_workflow(&self, id: &str) -> AppResult<()> { + pub async fn delete_workflow(&self, id: i64) -> AppResult<()> { super::model::delete_definition(&self.pool, id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}"))) @@ -103,7 +103,7 @@ impl WorkflowService { /// Start workflow instance pub async fn start_workflow( &self, - definition_id: &str, + definition_id: i64, context: &serde_json::Value, triggered_by: Option, ) -> AppResult { @@ -113,14 +113,14 @@ impl WorkflowService { return Err(AppError::BadRequest("workflow is disabled".into())); } - let id = uuid::Uuid::now_v7().to_string(); - let instance = create_instance(&self.pool, &id, def.id, context, triggered_by) + let id = crate::utils::id::new_id(); + let instance = create_instance(&self.pool, id, *def.id, context, triggered_by) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; update_instance_step( &self.pool, - &id, + id, WorkflowInstanceStatus::Running, Some(&def.initial_step), context, @@ -136,11 +136,11 @@ impl WorkflowService { .find(|s| s.id == def.initial_step) .ok_or_else(|| AppError::Internal(anyhow::anyhow!("initial step not found")))?; - let log_id = uuid::Uuid::now_v7().to_string(); + let log_id = crate::utils::id::new_id(); create_step_log( &self.pool, - &log_id, - instance.id, + log_id, + *instance.id, &initial.id, &initial.name, Some(context), @@ -163,7 +163,7 @@ impl WorkflowService { /// Once all branches complete, the engine automatically advances to the `join_next` step. pub async fn execute_step( &self, - instance_id: &str, + instance_id: i64, step_output: &serde_json::Value, ) -> AppResult { let instance = self @@ -182,7 +182,7 @@ impl WorkflowService { .as_deref() .ok_or_else(|| AppError::Internal(anyhow::anyhow!("no current step")))?; - let def = self.get_definition_by_pk(instance.definition_id).await?; + let def = self.get_definition_by_pk(*instance.definition_id).await?; let steps = def .parse_steps() .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; @@ -233,7 +233,7 @@ impl WorkflowService { ) -> AppResult { let current_step_id = ¤t_step.id; - let active_logs: Vec<_> = list_step_logs(&self.pool, instance.id) + let active_logs: Vec<_> = list_step_logs(&self.pool, *instance.id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? .into_iter() @@ -241,7 +241,7 @@ impl WorkflowService { .collect(); if let Some(log) = active_logs.first() { - complete_step_log(&self.pool, &log.document_id, Some(step_output)) + complete_step_log(&self.pool, *log.id, Some(step_output)) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; } @@ -255,7 +255,7 @@ impl WorkflowService { Some(ns) => { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Running, Some(&ns.id), context, @@ -263,11 +263,11 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - let log_id = uuid::Uuid::now_v7().to_string(); + let log_id = crate::utils::id::new_id(); create_step_log( &self.pool, - &log_id, - instance.id, + log_id, + *instance.id, &ns.id, &ns.name, Some(context), @@ -275,7 +275,7 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - self.get_instance(&instance.document_id) + self.get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")) } @@ -287,7 +287,7 @@ impl WorkflowService { None => { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Completed, None, context, @@ -295,7 +295,7 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - self.get_instance(&instance.document_id) + self.get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")) } @@ -344,14 +344,14 @@ impl WorkflowService { ))); } - let active_logs: Vec<_> = list_step_logs(&self.pool, instance.id) + let active_logs: Vec<_> = list_step_logs(&self.pool, *instance.id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? .into_iter() .filter(|l| l.step_id == parallel_step.id && (l.status == WorkflowStepStatus::Running)) .collect(); if let Some(log) = active_logs.first() { - complete_step_log(&self.pool, &log.document_id, None) + complete_step_log(&self.pool, *log.id, None) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; } @@ -378,11 +378,11 @@ impl WorkflowService { let branch_step = steps.iter().find(|s| s.id == *branch_id).ok_or_else(|| { AppError::Internal(anyhow::anyhow!("parallel branch '{}' not found", branch_id)) })?; - let log_id = uuid::Uuid::now_v7().to_string(); + let log_id = crate::utils::id::new_id(); create_step_log( &self.pool, - &log_id, - instance.id, + log_id, + *instance.id, &branch_step.id, &branch_step.name, Some(context), @@ -393,7 +393,7 @@ impl WorkflowService { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Running, Some(&first_branch.id), context, @@ -401,7 +401,7 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - self.get_instance(&instance.document_id) + self.get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")) } @@ -415,14 +415,14 @@ impl WorkflowService { context: &mut serde_json::Value, meta: &ParallelMeta, ) -> AppResult { - let active_logs: Vec<_> = list_step_logs(&self.pool, instance.id) + let active_logs: Vec<_> = list_step_logs(&self.pool, *instance.id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? .into_iter() .filter(|l| l.step_id == branch_step.id && (l.status == WorkflowStepStatus::Running)) .collect(); if let Some(log) = active_logs.first() { - complete_step_log(&self.pool, &log.document_id, None) + complete_step_log(&self.pool, *log.id, None) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; } @@ -451,7 +451,7 @@ impl WorkflowService { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Running, Some(&next_branch.id), context, @@ -460,7 +460,7 @@ impl WorkflowService { .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; return self - .get_instance(&instance.document_id) + .get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")); } @@ -480,7 +480,7 @@ impl WorkflowService { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Running, Some(&join_step.id), context, @@ -488,11 +488,11 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - let log_id = uuid::Uuid::now_v7().to_string(); + let log_id = crate::utils::id::new_id(); create_step_log( &self.pool, - &log_id, - instance.id, + log_id, + *instance.id, &join_step.id, &join_step.name, Some(context), @@ -500,14 +500,14 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - self.get_instance(&instance.document_id) + self.get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")) } None => { update_instance_step( &self.pool, - &instance.document_id, + *instance.id, WorkflowInstanceStatus::Completed, None, context, @@ -515,7 +515,7 @@ impl WorkflowService { .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; - self.get_instance(&instance.document_id) + self.get_instance(*instance.id) .await? .ok_or_else(|| AppError::not_found("workflow instance")) } @@ -523,14 +523,14 @@ impl WorkflowService { } /// Mark step as failed - pub async fn fail_step(&self, instance_id: &str, error: &str) -> AppResult<()> { + pub async fn fail_step(&self, instance_id: i64, error: &str) -> AppResult<()> { let instance = self .get_instance(instance_id) .await? .ok_or_else(|| AppError::not_found("workflow instance"))?; if let Some(ref step_id) = instance.current_step { - let active_logs: Vec<_> = list_step_logs(&self.pool, instance.id) + let active_logs: Vec<_> = list_step_logs(&self.pool, *instance.id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? .into_iter() @@ -538,7 +538,7 @@ impl WorkflowService { .collect(); if let Some(log) = active_logs.first() { - fail_step_log(&self.pool, &log.document_id, error) + fail_step_log(&self.pool, *log.id, error) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))?; } @@ -558,7 +558,7 @@ impl WorkflowService { } /// Cancel workflow instance - pub async fn cancel_instance(&self, instance_id: &str) -> AppResult<()> { + pub async fn cancel_instance(&self, instance_id: i64) -> AppResult<()> { let instance = self .get_instance(instance_id) .await? @@ -584,7 +584,7 @@ impl WorkflowService { } /// Get workflow instance - pub async fn get_instance(&self, id: &str) -> AppResult> { + pub async fn get_instance(&self, id: i64) -> AppResult> { get_instance(&self.pool, id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}"))) @@ -593,27 +593,23 @@ impl WorkflowService { /// List workflow instances pub async fn list_instances( &self, - definition_id: Option<&str>, + definition_id: Option, status: Option, page: i64, page_size: i64, ) -> AppResult<(Vec, i64)> { - let def_id: Option = match definition_id { - Some(did) => Some(self.get_workflow(did).await?.id), - None => None, - }; - super::model::list_instances(&self.pool, def_id, status, page, page_size) + super::model::list_instances(&self.pool, definition_id, status, page, page_size) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}"))) } /// Get step logs - pub async fn get_step_logs(&self, instance_id: &str) -> AppResult> { + pub async fn get_step_logs(&self, instance_id: i64) -> AppResult> { let instance = self .get_instance(instance_id) .await? .ok_or_else(|| AppError::not_found("workflow instance"))?; - list_step_logs(&self.pool, instance.id) + list_step_logs(&self.pool, *instance.id) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}"))) } @@ -702,20 +698,23 @@ mod tests { async fn create_workflow_success() { let svc = setup().await; let steps = vec![task_step("s1", "Step 1", "")]; + let wf_id = crate::utils::id::new_id(); let def = svc - .create_workflow("wf-1", "Test WF", Some("desc"), &steps) + .create_workflow(wf_id, "Test WF", Some("desc"), &steps) .await .unwrap(); assert_eq!(def.name, "Test WF"); assert_eq!(def.initial_step, "s1"); - assert_eq!(def.document_id, "wf-1"); + assert_eq!(def.id, wf_id); assert!(def.enabled); } #[tokio::test] async fn create_workflow_empty_steps_rejected() { let svc = setup().await; - let result = svc.create_workflow("wf-empty", "Empty", None, &[]).await; + let result = svc + .create_workflow(crate::utils::id::new_id(), "Empty", None, &[]) + .await; assert!(result.is_err()); } @@ -723,17 +722,18 @@ mod tests { async fn get_workflow_found() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-get", "Get", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Get", None, &steps) .await .unwrap(); - let def = svc.get_workflow("wf-get").await.unwrap(); + let def = svc.get_workflow(wf_id).await.unwrap(); assert_eq!(def.name, "Get"); } #[tokio::test] async fn get_workflow_not_found() { let svc = setup().await; - let result = svc.get_workflow("nonexistent").await; + let result = svc.get_workflow(9999999).await; assert!(result.is_err()); } @@ -741,10 +741,10 @@ mod tests { async fn list_workflows() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-a", "A", None, &steps) + svc.create_workflow(crate::utils::id::new_id(), "A", None, &steps) .await .unwrap(); - svc.create_workflow("wf-b", "B", None, &steps) + svc.create_workflow(crate::utils::id::new_id(), "B", None, &steps) .await .unwrap(); let list = svc.list_workflows().await.unwrap(); @@ -755,23 +755,25 @@ mod tests { async fn delete_workflow() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-del", "Del", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Del", None, &steps) .await .unwrap(); - svc.delete_workflow("wf-del").await.unwrap(); - assert!(svc.get_workflow("wf-del").await.is_err()); + svc.delete_workflow(wf_id).await.unwrap(); + assert!(svc.get_workflow(wf_id).await.is_err()); } #[tokio::test] async fn start_workflow_success() { let svc = setup().await; let steps = vec![task_step("s1", "Step 1", "")]; - svc.create_workflow("wf-start", "Start", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Start", None, &steps) .await .unwrap(); let inst = svc - .start_workflow("wf-start", &json!({"key": "val"}), None) + .start_workflow(wf_id, &json!({"key": "val"}), None) .await .unwrap(); assert_eq!(inst.status, WorkflowInstanceStatus::Running); @@ -781,7 +783,7 @@ mod tests { #[tokio::test] async fn start_workflow_not_found() { let svc = setup().await; - let result = svc.start_workflow("nope", &json!({}), None).await; + let result = svc.start_workflow(9999999, &json!({}), None).await; assert!(result.is_err()); } @@ -789,8 +791,9 @@ mod tests { async fn start_workflow_disabled() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; + let wf_id = crate::utils::id::new_id(); let def = svc - .create_workflow("wf-dis", "Dis", None, &steps) + .create_workflow(wf_id, "Dis", None, &steps) .await .unwrap(); sqlx::query("UPDATE workflow_definitions SET enabled = 0 WHERE id = ?") @@ -799,7 +802,7 @@ mod tests { .await .unwrap(); - let result = svc.start_workflow("wf-dis", &json!({}), None).await; + let result = svc.start_workflow(wf_id, &json!({}), None).await; assert!(result.is_err()); } @@ -807,18 +810,16 @@ mod tests { async fn execute_step_completes_single_task() { let svc = setup().await; let steps = vec![task_step("s1", "Only", "")]; - svc.create_workflow("wf-exec1", "Exec", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Exec", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-exec1", &json!({}), None) - .await - .unwrap(); - let inst_id = &inst.document_id; + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + let inst_id = inst.id; let result = svc - .execute_step(inst_id, &json!({"done": true})) + .execute_step(*inst_id, &json!({"done": true})) .await .unwrap(); assert_eq!(result.status, WorkflowInstanceStatus::Completed); @@ -832,17 +833,15 @@ mod tests { task_step("s1", "First", "s2"), task_step("s2", "Second", ""), ]; - svc.create_workflow("wf-exec2", "Chain", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Chain", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-exec2", &json!({}), None) - .await - .unwrap(); - let inst_id = inst.document_id.clone(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + let inst_id = inst.id; - let result = svc.execute_step(&inst_id, &json!({})).await.unwrap(); + let result = svc.execute_step(*inst_id, &json!({})).await.unwrap(); assert_eq!(result.current_step, Some("s2".to_string())); assert_eq!(result.status, WorkflowInstanceStatus::Running); } @@ -851,23 +850,22 @@ mod tests { async fn execute_step_not_running_rejected() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-nr", "NR", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "NR", None, &steps) .await .unwrap(); - let inst = svc.start_workflow("wf-nr", &json!({}), None).await.unwrap(); - svc.execute_step(&inst.document_id, &json!({})) - .await - .unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.execute_step(*inst.id, &json!({})).await.unwrap(); - let result = svc.execute_step(&inst.document_id, &json!({})).await; + let result = svc.execute_step(*inst.id, &json!({})).await; assert!(result.is_err()); } #[tokio::test] async fn execute_step_instance_not_found() { let svc = setup().await; - let result = svc.execute_step("nope", &json!({})).await; + let result = svc.execute_step(9999999, &json!({})).await; assert!(result.is_err()); } @@ -886,13 +884,14 @@ mod tests { task_step("s2", "Yes", ""), task_step("s3", "No", ""), ]; - svc.create_workflow("wf-br", "Branch", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Branch", None, &steps) .await .unwrap(); - let inst = svc.start_workflow("wf-br", &json!({}), None).await.unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); let result = svc - .execute_step(&inst.document_id, &json!({"ok": true})) + .execute_step(*inst.id, &json!({"ok": true})) .await .unwrap(); assert_eq!(result.current_step, Some("s2".to_string())); @@ -913,16 +912,14 @@ mod tests { task_step("s2", "Yes", ""), task_step("s3", "No", ""), ]; - svc.create_workflow("wf-br2", "Branch2", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Branch2", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-br2", &json!({}), None) - .await - .unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); let result = svc - .execute_step(&inst.document_id, &json!({"ok": false})) + .execute_step(*inst.id, &json!({"ok": false})) .await .unwrap(); assert_eq!(result.current_step, Some("s3".to_string())); @@ -937,26 +934,24 @@ mod tests { task_step("s3", "B", ""), task_step("s4", "After", ""), ]; - svc.create_workflow("wf-par", "Par", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Par", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-par", &json!({}), None) - .await - .unwrap(); - let iid = inst.document_id.clone(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + let iid = inst.id; - let r = svc.execute_step(&iid, &json!({})).await.unwrap(); + let r = svc.execute_step(*iid, &json!({})).await.unwrap(); assert_eq!(r.current_step, Some("s2".to_string())); - let r = svc.execute_step(&iid, &json!({"a": 1})).await.unwrap(); + let r = svc.execute_step(*iid, &json!({"a": 1})).await.unwrap(); assert_eq!(r.current_step, Some("s3".to_string())); - let r = svc.execute_step(&iid, &json!({"b": 2})).await.unwrap(); + let r = svc.execute_step(*iid, &json!({"b": 2})).await.unwrap(); assert_eq!(r.current_step, Some("s4".to_string())); - let r = svc.execute_step(&iid, &json!({})).await.unwrap(); + let r = svc.execute_step(*iid, &json!({})).await.unwrap(); assert_eq!(r.status, WorkflowInstanceStatus::Completed); } @@ -968,19 +963,17 @@ mod tests { task_step("s2", "A", ""), task_step("s3", "B", ""), ]; - svc.create_workflow("wf-par2", "Par2", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Par2", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-par2", &json!({}), None) - .await - .unwrap(); - let iid = inst.document_id.clone(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + let iid = inst.id; - svc.execute_step(&iid, &json!({})).await.unwrap(); - svc.execute_step(&iid, &json!({})).await.unwrap(); - let r = svc.execute_step(&iid, &json!({})).await.unwrap(); + svc.execute_step(*iid, &json!({})).await.unwrap(); + svc.execute_step(*iid, &json!({})).await.unwrap(); + let r = svc.execute_step(*iid, &json!({})).await.unwrap(); assert_eq!(r.status, WorkflowInstanceStatus::Completed); } @@ -988,24 +981,22 @@ mod tests { async fn fail_step_marks_failed() { let svc = setup().await; let steps = vec![await_step("s1", "Wait", "")]; - svc.create_workflow("wf-fail", "Fail", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Fail", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-fail", &json!({}), None) - .await - .unwrap(); - svc.fail_step(&inst.document_id, "timeout").await.unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.fail_step(*inst.id, "timeout").await.unwrap(); - let inst = svc.get_instance(&inst.document_id).await.unwrap().unwrap(); + let inst = svc.get_instance(*inst.id).await.unwrap().unwrap(); assert_eq!(inst.status, WorkflowInstanceStatus::Failed); } #[tokio::test] async fn fail_step_not_found() { let svc = setup().await; - let result = svc.fail_step("nope", "err").await; + let result = svc.fail_step(9999999, "err").await; assert!(result.is_err()); } @@ -1013,17 +1004,15 @@ mod tests { async fn cancel_instance() { let svc = setup().await; let steps = vec![await_step("s1", "Wait", "")]; - svc.create_workflow("wf-can", "Cancel", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Cancel", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-can", &json!({}), None) - .await - .unwrap(); - svc.cancel_instance(&inst.document_id).await.unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.cancel_instance(*inst.id).await.unwrap(); - let inst = svc.get_instance(&inst.document_id).await.unwrap().unwrap(); + let inst = svc.get_instance(*inst.id).await.unwrap().unwrap(); assert_eq!(inst.status, WorkflowInstanceStatus::Cancelled); } @@ -1031,19 +1020,15 @@ mod tests { async fn cancel_not_running_rejected() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-can2", "Cancel2", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Cancel2", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-can2", &json!({}), None) - .await - .unwrap(); - svc.execute_step(&inst.document_id, &json!({})) - .await - .unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.execute_step(*inst.id, &json!({})).await.unwrap(); - let result = svc.cancel_instance(&inst.document_id).await; + let result = svc.cancel_instance(*inst.id).await; assert!(result.is_err()); } @@ -1051,19 +1036,15 @@ mod tests { async fn get_step_logs_returns_entries() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-logs", "Logs", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Logs", None, &steps) .await .unwrap(); - let inst = svc - .start_workflow("wf-logs", &json!({}), None) - .await - .unwrap(); - svc.execute_step(&inst.document_id, &json!({"x": 1})) - .await - .unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.execute_step(*inst.id, &json!({"x": 1})).await.unwrap(); - let logs = svc.get_step_logs(&inst.document_id).await.unwrap(); + let logs = svc.get_step_logs(*inst.id).await.unwrap(); assert!(!logs.is_empty()); assert_eq!(logs[0].step_id, "s1"); assert_eq!(logs[0].status, WorkflowStepStatus::Completed); @@ -1072,7 +1053,7 @@ mod tests { #[tokio::test] async fn get_step_logs_instance_not_found() { let svc = setup().await; - let result = svc.get_step_logs("nope").await; + let result = svc.get_step_logs(9999999).await; assert!(result.is_err()); } @@ -1080,17 +1061,15 @@ mod tests { async fn list_instances_by_definition() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-li", "List", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "List", None, &steps) .await .unwrap(); - svc.start_workflow("wf-li", &json!({}), None).await.unwrap(); - svc.start_workflow("wf-li", &json!({}), None).await.unwrap(); + svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); - let (items, total) = svc - .list_instances(Some("wf-li"), None, 1, 10) - .await - .unwrap(); + let (items, total) = svc.list_instances(Some(wf_id), None, 1, 10).await.unwrap(); assert_eq!(total, 2); assert_eq!(items.len(), 2); } @@ -1099,14 +1078,13 @@ mod tests { async fn list_instances_by_status() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-ls", "Status", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Status", None, &steps) .await .unwrap(); - let inst = svc.start_workflow("wf-ls", &json!({}), None).await.unwrap(); - svc.execute_step(&inst.document_id, &json!({})) - .await - .unwrap(); + let inst = svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); + svc.execute_step(*inst.id, &json!({})).await.unwrap(); let (running, _) = svc .list_instances(None, Some(WorkflowInstanceStatus::Running), 1, 10) @@ -1126,27 +1104,20 @@ mod tests { async fn list_instances_pagination() { let svc = setup().await; let steps = vec![task_step("s1", "S1", "")]; - svc.create_workflow("wf-page", "Page", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Page", None, &steps) .await .unwrap(); for _ in 0..5 { - svc.start_workflow("wf-page", &json!({}), None) - .await - .unwrap(); + svc.start_workflow(wf_id, &json!({}), None).await.unwrap(); } - let (page1, total) = svc - .list_instances(Some("wf-page"), None, 1, 2) - .await - .unwrap(); + let (page1, total) = svc.list_instances(Some(wf_id), None, 1, 2).await.unwrap(); assert_eq!(total, 5); assert_eq!(page1.len(), 2); - let (page2, _) = svc - .list_instances(Some("wf-page"), None, 2, 2) - .await - .unwrap(); + let (page2, _) = svc.list_instances(Some(wf_id), None, 2, 2).await.unwrap(); assert_eq!(page2.len(), 2); } @@ -1157,24 +1128,25 @@ mod tests { task_step("s1", "First", "s2"), task_step("s2", "Second", ""), ]; - svc.create_workflow("wf-ctx", "Ctx", None, &steps) + let wf_id = crate::utils::id::new_id(); + svc.create_workflow(wf_id, "Ctx", None, &steps) .await .unwrap(); let inst = svc - .start_workflow("wf-ctx", &json!({"init": 1}), None) + .start_workflow(wf_id, &json!({"init": 1}), None) .await .unwrap(); - let iid = inst.document_id.clone(); + let iid = inst.id; - svc.execute_step(&iid, &json!({"step1": "done"})) + svc.execute_step(*iid, &json!({"step1": "done"})) .await .unwrap(); - svc.execute_step(&iid, &json!({"step2": "done"})) + svc.execute_step(*iid, &json!({"step2": "done"})) .await .unwrap(); - let inst = svc.get_instance(&iid).await.unwrap().unwrap(); + let inst = svc.get_instance(*iid).await.unwrap().unwrap(); let ctx = inst.parse_context(); assert_eq!(ctx["init"], 1); assert_eq!(ctx["step1"], "done"); diff --git a/src/workflow/handler.rs b/src/workflow/handler.rs index f14f21dd..e51a8c16 100644 --- a/src/workflow/handler.rs +++ b/src/workflow/handler.rs @@ -158,14 +158,10 @@ pub async fn create( State(state): State, Json(body): Json, ) -> AppResult> { + let wf_id: i64 = crate::utils::id::parse_id(&body.id)?; let wf = state .workflow - .create_workflow( - &body.id, - &body.name, - body.description.as_deref(), - &body.steps, - ) + .create_workflow(wf_id, &body.name, body.description.as_deref(), &body.steps) .await?; Ok(ApiResponse::success( serde_json::to_value(wf).unwrap_or_default(), @@ -183,7 +179,8 @@ pub async fn get( State(state): State, Path(id): Path, ) -> AppResult> { - let wf = state.workflow.get_workflow(&id).await?; + let wf_id: i64 = crate::utils::id::parse_id(&id)?; + let wf = state.workflow.get_workflow(wf_id).await?; Ok(ApiResponse::success( serde_json::to_value(wf).unwrap_or_default(), )) @@ -193,7 +190,8 @@ pub async fn delete( State(state): State, Path(id): Path, ) -> AppResult> { - state.workflow.delete_workflow(&id).await?; + let wf_id: i64 = crate::utils::id::parse_id(&id)?; + state.workflow.delete_workflow(wf_id).await?; Ok(ApiResponse::success(())) } @@ -202,14 +200,12 @@ pub async fn start( Path(id): Path, Json(body): Json, ) -> AppResult> { + let wf_id: i64 = crate::utils::id::parse_id(&id)?; let triggered_by_int: Option = match &body.triggered_by { - Some(doc_id) if !doc_id.is_empty() => { - let sql = format!( - "SELECT id FROM users WHERE document_id = {}", - dialect::ph(1) - ); + Some(uid) if !uid.is_empty() => { + let sql = format!("SELECT id FROM users WHERE id = {}", dialect::ph(1)); sqlx::query_scalar::<_, i64>(&sql) - .bind(doc_id) + .bind(uid.parse::().unwrap_or(0)) .fetch_optional(&state.pool) .await .map_err(|e| AppError::Internal(anyhow::anyhow!("{e}")))? @@ -219,7 +215,7 @@ pub async fn start( let instance = state .workflow - .start_workflow(&id, &body.context, triggered_by_int) + .start_workflow(wf_id, &body.context, triggered_by_int) .await?; Ok(ApiResponse::success( serde_json::to_value(instance).unwrap_or_default(), @@ -232,14 +228,13 @@ pub async fn list_instances( ) -> AppResult> { let page = query.page.unwrap_or(1).max(1); let page_size = query.page_size.unwrap_or(20).clamp(1, 100); + let def_id: Option = match &query.definition_id { + Some(did) => Some(crate::utils::id::parse_id(did)?), + None => None, + }; let (items, total) = state .workflow - .list_instances( - query.definition_id.as_deref(), - query.status, - page, - page_size, - ) + .list_instances(def_id, query.status, page, page_size) .await?; Ok(ApiResponse::success(json!({ "items": items, @@ -253,9 +248,10 @@ pub async fn get_instance( State(state): State, Path(id): Path, ) -> AppResult> { + let inst_id: i64 = crate::utils::id::parse_id(&id)?; let instance = state .workflow - .get_instance(&id) + .get_instance(inst_id) .await? .ok_or_else(|| AppError::not_found("workflow instance"))?; Ok(ApiResponse::success( @@ -268,7 +264,8 @@ pub async fn execute_step( Path(id): Path, Json(body): Json, ) -> AppResult> { - let instance = state.workflow.execute_step(&id, &body.output).await?; + let inst_id: i64 = crate::utils::id::parse_id(&id)?; + let instance = state.workflow.execute_step(inst_id, &body.output).await?; Ok(ApiResponse::success( serde_json::to_value(instance).unwrap_or_default(), )) @@ -278,7 +275,8 @@ pub async fn cancel_instance( State(state): State, Path(id): Path, ) -> AppResult> { - state.workflow.cancel_instance(&id).await?; + let inst_id: i64 = crate::utils::id::parse_id(&id)?; + state.workflow.cancel_instance(inst_id).await?; Ok(ApiResponse::success(())) } @@ -286,7 +284,8 @@ pub async fn get_step_logs( State(state): State, Path(id): Path, ) -> AppResult> { - let logs = state.workflow.get_step_logs(&id).await?; + let inst_id: i64 = crate::utils::id::parse_id(&id)?; + let logs = state.workflow.get_step_logs(inst_id).await?; Ok(ApiResponse::success( serde_json::to_value(logs).unwrap_or_default(), )) diff --git a/src/workflow/model.rs b/src/workflow/model.rs index 7aa82ef5..ea843e59 100644 --- a/src/workflow/model.rs +++ b/src/workflow/model.rs @@ -6,6 +6,7 @@ use ts_rs::TS; use crate::db::Pool; use crate::db::dialect::ph; +use crate::utils::id::SnowflakeId; use crate::utils::tz::Timestamp; define_enum!( @@ -67,8 +68,7 @@ pub struct StepDef { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct WorkflowDefinition { - pub id: i64, - pub document_id: String, + pub id: SnowflakeId, pub name: String, pub description: Option, pub steps: String, @@ -90,13 +90,12 @@ impl WorkflowDefinition { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct WorkflowInstance { - pub id: i64, - pub document_id: String, - pub definition_id: i64, + pub id: SnowflakeId, + pub definition_id: SnowflakeId, pub status: WorkflowInstanceStatus, pub current_step: Option, pub context: String, - pub triggered_by: Option, + pub triggered_by: Option, pub started_at: Timestamp, pub completed_at: Option, pub updated_at: Timestamp, @@ -113,9 +112,8 @@ impl WorkflowInstance { #[cfg_attr(feature = "export-types", derive(TS))] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct StepLog { - pub id: i64, - pub document_id: String, - pub instance_id: i64, + pub id: SnowflakeId, + pub instance_id: SnowflakeId, pub step_id: String, pub step_name: String, pub status: WorkflowStepStatus, @@ -129,7 +127,7 @@ pub struct StepLog { /// Create workflow definition pub async fn create_definition( pool: &Pool, - document_id: &str, + id: i64, name: &str, description: Option<&str>, steps: &str, @@ -137,7 +135,7 @@ pub async fn create_definition( ) -> anyhow::Result { let now = crate::utils::tz::now_utc(); let sql = format!( - "INSERT INTO workflow_definitions (document_id, name, description, steps, initial_step, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {})", + "INSERT INTO workflow_definitions (id, name, description, steps, initial_step, created_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), @@ -147,7 +145,7 @@ pub async fn create_definition( ph(7) ); sqlx::query(&sql) - .bind(document_id) + .bind(id) .bind(name) .bind(description) .bind(steps) @@ -156,22 +154,19 @@ pub async fn create_definition( .bind(now) .execute(pool) .await?; - get_definition(pool, document_id) + get_definition(pool, id) .await? .ok_or_else(|| anyhow::anyhow!("failed to fetch created workflow definition")) } /// Get workflow definition -pub async fn get_definition( - pool: &Pool, - document_id: &str, -) -> anyhow::Result> { +pub async fn get_definition(pool: &Pool, id: i64) -> anyhow::Result> { let sql = format!( - "SELECT id, document_id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions WHERE document_id = {}", + "SELECT id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions WHERE id = {}", ph(1) ); let row = sqlx::query_as::<_, WorkflowDefinition>(&sql) - .bind(document_id) + .bind(id) .fetch_optional(pool) .await?; Ok(row) @@ -179,7 +174,7 @@ pub async fn get_definition( /// List all workflow definitions pub async fn list_definitions(pool: &Pool) -> anyhow::Result> { - let sql = "SELECT id, document_id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions ORDER BY created_at DESC"; + let sql = "SELECT id, name, description, steps, initial_step, version, enabled, created_at, updated_at FROM workflow_definitions ORDER BY created_at DESC"; let rows = sqlx::query_as::<_, WorkflowDefinition>(sql) .fetch_all(pool) .await?; @@ -187,19 +182,16 @@ pub async fn list_definitions(pool: &Pool) -> anyhow::Result anyhow::Result<()> { - let sql = format!( - "DELETE FROM workflow_definitions WHERE document_id = {}", - ph(1) - ); - sqlx::query(&sql).bind(document_id).execute(pool).await?; +pub async fn delete_definition(pool: &Pool, id: i64) -> anyhow::Result<()> { + let sql = format!("DELETE FROM workflow_definitions WHERE id = {}", ph(1)); + sqlx::query(&sql).bind(id).execute(pool).await?; Ok(()) } /// Create workflow instance pub async fn create_instance( pool: &Pool, - document_id: &str, + id: i64, definition_id: i64, context: &serde_json::Value, triggered_by: Option, @@ -207,7 +199,7 @@ pub async fn create_instance( let now = crate::utils::tz::now_utc(); let ctx_str = serde_json::to_string(context)?; let sql = format!( - "INSERT INTO workflow_instances (document_id, definition_id, status, context, triggered_by, started_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {})", + "INSERT INTO workflow_instances (id, definition_id, status, context, triggered_by, started_at, updated_at) VALUES ({}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), @@ -217,7 +209,7 @@ pub async fn create_instance( ph(7) ); sqlx::query(&sql) - .bind(document_id) + .bind(id) .bind(definition_id) .bind(WorkflowInstanceStatus::Running) .bind(&ctx_str) @@ -226,22 +218,19 @@ pub async fn create_instance( .bind(now) .execute(pool) .await?; - get_instance(pool, document_id) + get_instance(pool, id) .await? .ok_or_else(|| anyhow::anyhow!("failed to fetch created workflow instance")) } /// Get workflow instance -pub async fn get_instance( - pool: &Pool, - document_id: &str, -) -> anyhow::Result> { +pub async fn get_instance(pool: &Pool, id: i64) -> anyhow::Result> { let sql = format!( - "SELECT id, document_id, definition_id, status, current_step, context, triggered_by, started_at, completed_at, updated_at FROM workflow_instances WHERE document_id = {}", + "SELECT id, definition_id, status, current_step, context, triggered_by, started_at, completed_at, updated_at FROM workflow_instances WHERE id = {}", ph(1) ); let row = sqlx::query_as::<_, WorkflowInstance>(&sql) - .bind(document_id) + .bind(id) .fetch_optional(pool) .await?; Ok(row) @@ -257,7 +246,7 @@ pub async fn list_instances( ) -> anyhow::Result<(Vec, i64)> { let offset = (page - 1) * page_size; let sql = format!( - "SELECT id, document_id, definition_id, status, current_step, context, triggered_by, started_at, completed_at, updated_at FROM workflow_instances WHERE ({} IS NULL OR definition_id = {}) AND ({} IS NULL OR status = {}) ORDER BY started_at DESC LIMIT {} OFFSET {}", + "SELECT id, definition_id, status, current_step, context, triggered_by, started_at, completed_at, updated_at FROM workflow_instances WHERE ({} IS NULL OR definition_id = {}) AND ({} IS NULL OR status = {}) ORDER BY started_at DESC LIMIT {} OFFSET {}", ph(1), ph(2), ph(3), @@ -296,7 +285,7 @@ pub async fn list_instances( /// Update instance status and current step pub async fn update_instance_step( pool: &Pool, - document_id: &str, + id: i64, status: WorkflowInstanceStatus, current_step: Option<&str>, context: &serde_json::Value, @@ -312,7 +301,7 @@ pub async fn update_instance_step( None }; let sql = format!( - "UPDATE workflow_instances SET status = {}, current_step = {}, context = {}, completed_at = COALESCE({}, completed_at), updated_at = {} WHERE document_id = {}", + "UPDATE workflow_instances SET status = {}, current_step = {}, context = {}, completed_at = COALESCE({}, completed_at), updated_at = {} WHERE id = {}", ph(1), ph(2), ph(3), @@ -326,7 +315,7 @@ pub async fn update_instance_step( .bind(&ctx_str) .bind(completed_at) .bind(now) - .bind(document_id) + .bind(id) .execute(pool) .await?; Ok(()) @@ -335,7 +324,7 @@ pub async fn update_instance_step( /// Create step execution log pub async fn create_step_log( pool: &Pool, - document_id: &str, + id: i64, instance_id: i64, step_id: &str, step_name: &str, @@ -344,7 +333,7 @@ pub async fn create_step_log( let now = crate::utils::tz::now_utc(); let input_str = input.map(|v| serde_json::to_string(v).unwrap_or_default()); let sql = format!( - "INSERT INTO workflow_step_logs (document_id, instance_id, step_id, step_name, status, input, started_at) VALUES ({}, {}, {}, {}, {}, {}, {})", + "INSERT INTO workflow_step_logs (id, instance_id, step_id, step_name, status, input, started_at) VALUES ({}, {}, {}, {}, {}, {}, {})", ph(1), ph(2), ph(3), @@ -354,7 +343,7 @@ pub async fn create_step_log( ph(7) ); sqlx::query(&sql) - .bind(document_id) + .bind(id) .bind(instance_id) .bind(step_id) .bind(step_name) @@ -364,11 +353,11 @@ pub async fn create_step_log( .execute(pool) .await?; let fetch_sql = format!( - "SELECT id, document_id, instance_id, step_id, step_name, status, input, output, error, started_at, completed_at FROM workflow_step_logs WHERE document_id = {}", + "SELECT id, instance_id, step_id, step_name, status, input, output, error, started_at, completed_at FROM workflow_step_logs WHERE id = {}", ph(1) ); sqlx::query_as::<_, StepLog>(&fetch_sql) - .bind(document_id) + .bind(id) .fetch_one(pool) .await .map_err(|e| anyhow::anyhow!("failed to fetch created step log: {e}")) @@ -377,13 +366,13 @@ pub async fn create_step_log( /// Complete step execution log pub async fn complete_step_log( pool: &Pool, - document_id: &str, + id: i64, output: Option<&serde_json::Value>, ) -> anyhow::Result<()> { let now = crate::utils::tz::now_utc(); let output_str = output.map(|v| serde_json::to_string(v).unwrap_or_default()); let sql = format!( - "UPDATE workflow_step_logs SET status = {}, output = {}, completed_at = {} WHERE document_id = {}", + "UPDATE workflow_step_logs SET status = {}, output = {}, completed_at = {} WHERE id = {}", ph(1), ph(2), ph(3), @@ -393,17 +382,17 @@ pub async fn complete_step_log( .bind(WorkflowStepStatus::Completed) .bind(&output_str) .bind(now) - .bind(document_id) + .bind(id) .execute(pool) .await?; Ok(()) } /// Mark step execution as failed -pub async fn fail_step_log(pool: &Pool, document_id: &str, error: &str) -> anyhow::Result<()> { +pub async fn fail_step_log(pool: &Pool, id: i64, error: &str) -> anyhow::Result<()> { let now = crate::utils::tz::now_utc(); let sql = format!( - "UPDATE workflow_step_logs SET status = {}, error = {}, completed_at = {} WHERE document_id = {}", + "UPDATE workflow_step_logs SET status = {}, error = {}, completed_at = {} WHERE id = {}", ph(1), ph(2), ph(3), @@ -413,7 +402,7 @@ pub async fn fail_step_log(pool: &Pool, document_id: &str, error: &str) -> anyho .bind(WorkflowStepStatus::Failed) .bind(error) .bind(now) - .bind(document_id) + .bind(id) .execute(pool) .await?; Ok(()) @@ -422,7 +411,7 @@ pub async fn fail_step_log(pool: &Pool, document_id: &str, error: &str) -> anyho /// List step logs for an instance pub async fn list_step_logs(pool: &Pool, instance_id: i64) -> anyhow::Result> { let sql = format!( - "SELECT id, document_id, instance_id, step_id, step_name, status, input, output, error, started_at, completed_at FROM workflow_step_logs WHERE instance_id = {} ORDER BY started_at ASC", + "SELECT id, instance_id, step_id, step_name, status, input, output, error, started_at, completed_at FROM workflow_step_logs WHERE instance_id = {} ORDER BY started_at ASC", ph(1) ); let rows = sqlx::query_as::<_, StepLog>(&sql) @@ -443,18 +432,14 @@ mod tests { #[tokio::test] async fn create_and_get_definition() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "Test WF", Some("desc"), steps, "s1") + let def = super::create_definition(&pool, id, "Test WF", Some("desc"), steps, "s1") .await .unwrap(); - assert_eq!(def.document_id, doc_id); assert_eq!(def.name, "Test WF"); - let got = super::get_definition(&pool, &doc_id) - .await - .unwrap() - .unwrap(); + let got = super::get_definition(&pool, id).await.unwrap().unwrap(); assert_eq!(got.id, def.id); } @@ -463,8 +448,8 @@ mod tests { let pool = setup_pool().await; let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; for i in 0..3 { - let doc_id = crate::utils::id::new_document_id(); - super::create_definition(&pool, &doc_id, &format!("WF {i}"), None, steps, "s1") + let id = crate::utils::id::new_id(); + super::create_definition(&pool, id, &format!("WF {i}"), None, steps, "s1") .await .unwrap(); } @@ -475,64 +460,55 @@ mod tests { #[tokio::test] async fn delete_definition() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - super::create_definition(&pool, &doc_id, "To delete", None, steps, "s1") + super::create_definition(&pool, id, "To delete", None, steps, "s1") .await .unwrap(); - super::delete_definition(&pool, &doc_id).await.unwrap(); - assert!( - super::get_definition(&pool, &doc_id) - .await - .unwrap() - .is_none() - ); + super::delete_definition(&pool, id).await.unwrap(); + assert!(super::get_definition(&pool, id).await.unwrap().is_none()); } #[tokio::test] async fn create_and_get_instance() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); + let inst_id = crate::utils::id::new_id(); let ctx = serde_json::json!({"key": "value"}); - let inst = super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None) + let inst = super::create_instance(&pool, inst_id, *def.id, &ctx, None) .await .unwrap(); - assert_eq!(inst.document_id, inst_doc_id); assert_eq!(inst.definition_id, def.id); assert_eq!(inst.status, WorkflowInstanceStatus::Running); - let got = super::get_instance(&pool, &inst_doc_id) - .await - .unwrap() - .unwrap(); + let got = super::get_instance(&pool, inst_id).await.unwrap().unwrap(); assert_eq!(got.id, inst.id); } #[tokio::test] async fn list_instances_test() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); for _ in 0..3 { - let inst_doc_id = crate::utils::id::new_document_id(); + let inst_id = crate::utils::id::new_id(); let ctx = serde_json::json!({}); - super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None) + super::create_instance(&pool, inst_id, *def.id, &ctx, None) .await .unwrap(); } - let (rows, count) = super::list_instances(&pool, Some(def.id), None, 1, 10) + let (rows, count) = super::list_instances(&pool, Some(*def.id), None, 1, 10) .await .unwrap(); assert_eq!(rows.len(), 3); @@ -542,22 +518,22 @@ mod tests { #[tokio::test] async fn update_instance_step() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); + let inst_id = crate::utils::id::new_id(); let ctx = serde_json::json!({}); - super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None) + super::create_instance(&pool, inst_id, *def.id, &ctx, None) .await .unwrap(); let new_ctx = serde_json::json!({"progress": 50}); super::update_instance_step( &pool, - &inst_doc_id, + inst_id, WorkflowInstanceStatus::Paused, Some("s1"), &new_ctx, @@ -565,10 +541,7 @@ mod tests { .await .unwrap(); - let inst = super::get_instance(&pool, &inst_doc_id) - .await - .unwrap() - .unwrap(); + let inst = super::get_instance(&pool, inst_id).await.unwrap().unwrap(); assert_eq!(inst.status, WorkflowInstanceStatus::Paused); assert_eq!(inst.current_step, Some("s1".to_string())); } @@ -576,32 +549,32 @@ mod tests { #[tokio::test] async fn step_log_lifecycle() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); + let inst_id = crate::utils::id::new_id(); let ctx = serde_json::json!({}); - let inst = super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None) + let inst = super::create_instance(&pool, inst_id, *def.id, &ctx, None) .await .unwrap(); - let log_doc_id = crate::utils::id::new_document_id(); + let log_id = crate::utils::id::new_id(); let input = serde_json::json!({"data": 1}); - let log = super::create_step_log(&pool, &log_doc_id, inst.id, "s1", "Step 1", Some(&input)) + let log = super::create_step_log(&pool, log_id, *inst.id, "s1", "Step 1", Some(&input)) .await .unwrap(); assert_eq!(log.status, WorkflowStepStatus::Running); assert_eq!(log.step_id, "s1"); let output = serde_json::json!({"result": "ok"}); - super::complete_step_log(&pool, &log_doc_id, Some(&output)) + super::complete_step_log(&pool, log_id, Some(&output)) .await .unwrap(); - let logs = super::list_step_logs(&pool, inst.id).await.unwrap(); + let logs = super::list_step_logs(&pool, *inst.id).await.unwrap(); assert_eq!(logs.len(), 1); assert_eq!(logs[0].status, WorkflowStepStatus::Completed); assert!(logs[0].completed_at.is_some()); @@ -610,9 +583,9 @@ mod tests { #[tokio::test] async fn parse_steps_valid() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, id, "WF", None, steps, "s1") .await .unwrap(); @@ -625,9 +598,9 @@ mod tests { #[tokio::test] async fn parse_steps_invalid_json() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let id = crate::utils::id::new_id(); let steps = "not valid json!!!"; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, id, "WF", None, steps, "s1") .await .unwrap(); @@ -637,28 +610,27 @@ mod tests { #[tokio::test] async fn fail_step_log_marks_failed() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); - let inst = - super::create_instance(&pool, &inst_doc_id, def.id, &serde_json::json!({}), None) - .await - .unwrap(); - - let log_doc_id = crate::utils::id::new_document_id(); - super::create_step_log(&pool, &log_doc_id, inst.id, "s1", "Step 1", None) + let inst_id = crate::utils::id::new_id(); + let inst = super::create_instance(&pool, inst_id, *def.id, &serde_json::json!({}), None) .await .unwrap(); - super::fail_step_log(&pool, &log_doc_id, "something broke") + let log_id = crate::utils::id::new_id(); + super::create_step_log(&pool, log_id, *inst.id, "s1", "Step 1", None) .await .unwrap(); - let logs = super::list_step_logs(&pool, inst.id).await.unwrap(); + super::fail_step_log(&pool, log_id, "something broke") + .await + .unwrap(); + + let logs = super::list_step_logs(&pool, *inst.id).await.unwrap(); assert_eq!(logs[0].status, WorkflowStepStatus::Failed); assert_eq!(logs[0].error, Some("something broke".to_string())); assert!(logs[0].completed_at.is_some()); @@ -667,20 +639,20 @@ mod tests { #[tokio::test] async fn list_instances_filter_by_status() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); - super::create_instance(&pool, &inst_doc_id, def.id, &serde_json::json!({}), None) + let inst_id = crate::utils::id::new_id(); + super::create_instance(&pool, inst_id, *def.id, &serde_json::json!({}), None) .await .unwrap(); super::update_instance_step( &pool, - &inst_doc_id, + inst_id, WorkflowInstanceStatus::Completed, None, &serde_json::json!({}), @@ -690,7 +662,7 @@ mod tests { let (running, _) = super::list_instances( &pool, - Some(def.id), + Some(*def.id), Some(WorkflowInstanceStatus::Running), 1, 10, @@ -701,7 +673,7 @@ mod tests { let (completed, total) = super::list_instances( &pool, - Some(def.id), + Some(*def.id), Some(WorkflowInstanceStatus::Completed), 1, 10, @@ -715,26 +687,26 @@ mod tests { #[tokio::test] async fn list_instances_pagination() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); for _ in 0..5 { - let inst_doc_id = crate::utils::id::new_document_id(); - super::create_instance(&pool, &inst_doc_id, def.id, &serde_json::json!({}), None) + let inst_id = crate::utils::id::new_id(); + super::create_instance(&pool, inst_id, *def.id, &serde_json::json!({}), None) .await .unwrap(); } - let (page1, total) = super::list_instances(&pool, Some(def.id), None, 1, 2) + let (page1, total) = super::list_instances(&pool, Some(*def.id), None, 1, 2) .await .unwrap(); assert_eq!(total, 5); assert_eq!(page1.len(), 2); - let (page2, _) = super::list_instances(&pool, Some(def.id), None, 2, 2) + let (page2, _) = super::list_instances(&pool, Some(*def.id), None, 2, 2) .await .unwrap(); assert_eq!(page2.len(), 2); @@ -743,20 +715,20 @@ mod tests { #[tokio::test] async fn update_instance_completed_sets_completed_at() { let pool = setup_pool().await; - let doc_id = crate::utils::id::new_document_id(); + let def_id = crate::utils::id::new_id(); let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#; - let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1") + let def = super::create_definition(&pool, def_id, "WF", None, steps, "s1") .await .unwrap(); - let inst_doc_id = crate::utils::id::new_document_id(); - super::create_instance(&pool, &inst_doc_id, def.id, &serde_json::json!({}), None) + let inst_id = crate::utils::id::new_id(); + super::create_instance(&pool, inst_id, *def.id, &serde_json::json!({}), None) .await .unwrap(); super::update_instance_step( &pool, - &inst_doc_id, + inst_id, WorkflowInstanceStatus::Completed, None, &serde_json::json!({"done": true}), @@ -764,10 +736,7 @@ mod tests { .await .unwrap(); - let inst = super::get_instance(&pool, &inst_doc_id) - .await - .unwrap() - .unwrap(); + let inst = super::get_instance(&pool, inst_id).await.unwrap().unwrap(); assert_eq!(inst.status, WorkflowInstanceStatus::Completed); assert!(inst.completed_at.is_some()); let ctx = inst.parse_context(); @@ -777,14 +746,14 @@ mod tests { #[tokio::test] async fn get_definition_not_found() { let pool = setup_pool().await; - let result = super::get_definition(&pool, "nonexistent").await.unwrap(); + let result = super::get_definition(&pool, 9999999).await.unwrap(); assert!(result.is_none()); } #[tokio::test] async fn get_instance_not_found() { let pool = setup_pool().await; - let result = super::get_instance(&pool, "nonexistent").await.unwrap(); + let result = super::get_instance(&pool, 9999999).await.unwrap(); assert!(result.is_none()); } } diff --git a/templates/codegen/model.rs.tera b/templates/codegen/model.rs.tera new file mode 100644 index 00000000..6f7103ab --- /dev/null +++ b/templates/codegen/model.rs.tera @@ -0,0 +1,80 @@ +//! {{ singular }} model and database queries + +use serde::{Deserialize, Serialize}; +#[cfg(feature = "export-types")] +use ts_rs::TS; + +use crate::errors::app_error::{AppError, AppResult}; +{% if has_timestamp %} +use crate::utils::tz::Timestamp; +{% endif %} + +#[cfg_attr(feature = "export-types", derive(TS))] +#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] +pub struct {{ struct_name }} { +{% for col in columns %} + pub {{ col.name }}: {{ col.rust_type }}, +{% endfor %} +} + +pub async fn find_all( + pool: &crate::db::Pool{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %} +) -> AppResult> { + raisfast_derive::crud_list!(pool, "{{ name }}", {{ struct_name }}{% if has_tenant_id %}, tenant: tenant_id{% endif %}) + .map_err(Into::into) +} + +pub async fn find_paginated( + pool: &crate::db::Pool{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %}, + page: i64, + page_size: i64, +) -> AppResult<(Vec<{{ struct_name }}>, i64)> { + let result = raisfast_derive::crud_query_paged!( + pool, {{ struct_name }}, + data_sql: "SELECT * FROM {{ name }} WHERE 1=1{tenant}", + count_sql: "SELECT COUNT(*) FROM {{ name }} WHERE 1=1{tenant}", + binds: [], +{% if has_tenant_id %} + tenant: tenant_id, +{% endif %} + page: page, + page_size: page_size + ); + Ok(result) +} + +pub async fn find_by_id( + pool: &crate::db::Pool, + id: i64{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %} +) -> AppResult<{{ struct_name }}> { + raisfast_derive::crud_find_one!(pool, "{{ name }}", {{ struct_name }}, "id" => id{% if has_tenant_id %}, tenant: tenant_id{% endif %}) + .map_err(Into::into) +} + +pub async fn delete( + pool: &crate::db::Pool, + id: i64{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %} +) -> AppResult<()> { + let result = raisfast_derive::crud_delete!(pool, "{{ name }}", "id" => id{% if has_tenant_id %}, tenant: tenant_id{% endif %})?; + AppError::expect_affected(&result, "{{ singular }}") +} + +pub async fn create( + pool: &crate::db::Pool{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %} + // TODO: add parameters +) -> AppResult<{{ struct_name }}> { + todo!("implement create for {{ struct_name }}") +} + +pub async fn update( + pool: &crate::db::Pool{% if has_tenant_id %}, + tenant_id: Option<&str>{% endif %} + // TODO: add parameters +) -> AppResult<{{ struct_name }}> { + todo!("implement update for {{ struct_name }}") +} diff --git a/tests/api.rs b/tests/api.rs index fd1601dc..d341719f 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -411,7 +411,7 @@ async fn build_test_app(pool: raisfast::db::Pool) -> (axum::Router, AppState) { get(h_wallet::list_user_transactions), ) .route( - "/admin/wallets/{tx_doc_id}/reversal", + "/admin/wallets/{tx_id}/reversal", http_post(h_wallet::admin_reversal), ) .route( @@ -439,7 +439,7 @@ async fn build_test_app(pool: raisfast::db::Pool) -> (axum::Router, AppState) { get(h_payment::list_order_refunds), ) .route( - "/payment/callback/{channel_doc_id}", + "/payment/callback/{channel_id}", http_post(h_payment::handle_callback).layer(from_fn(payment_callback_rate_limit)), ) .route( @@ -584,11 +584,11 @@ pub(crate) fn delete_auth(path: &str, token: &str) -> Request { } pub(crate) fn make_token( - user_id: &str, + _user_id: &str, iid: i64, role: raisfast::models::user::UserRole, ) -> String { - raisfast::services::auth::generate_access_token_for_test(user_id, iid, role) + raisfast::services::auth::generate_access_token_for_test(iid, role) } pub(crate) async fn register_and_login( @@ -625,20 +625,12 @@ pub(crate) async fn register_and_login( pub(crate) async fn create_admin(pool: &raisfast::db::Pool) -> (i64, String) { let hash = raisfast::services::auth::hash_password("AdminPass123!").unwrap(); - let doc_id = uuid::Uuid::now_v7().to_string(); - let sql = format!( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES ({}, 'testadmin', 'admin', 'active', 'email') RETURNING id", - raisfast::db::dialect::ph(1) - ); - let int_id: i64 = sqlx::query_scalar(&sql) - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); + let sql = "INSERT INTO users (username, role, status, registered_via) VALUES ('testadmin', 'admin', 'active', 'email') RETURNING id"; + let int_id: i64 = sqlx::query_scalar(sql).fetch_one(pool).await.unwrap(); let cred_data = serde_json::json!({"password_hash": hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -647,7 +639,7 @@ pub(crate) async fn create_admin(pool: &raisfast::db::Pool) -> (i64, String) { raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(int_id) .bind("admin@test.com") .bind(&cred_data) @@ -656,25 +648,17 @@ pub(crate) async fn create_admin(pool: &raisfast::db::Pool) -> (i64, String) { .execute(pool) .await .unwrap(); - (int_id, doc_id) + (int_id, int_id.to_string()) } pub(crate) async fn create_author(pool: &raisfast::db::Pool) -> (i64, String) { let hash = raisfast::services::auth::hash_password("AuthorPass123!").unwrap(); - let doc_id = uuid::Uuid::now_v7().to_string(); - let sql = format!( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES ({}, 'testauthor', 'author', 'active', 'email') RETURNING id", - raisfast::db::dialect::ph(1) - ); - let int_id: i64 = sqlx::query_scalar(&sql) - .bind(&doc_id) - .fetch_one(pool) - .await - .unwrap(); + let sql = "INSERT INTO users (username, role, status, registered_via) VALUES ('testauthor', 'author', 'active', 'email') RETURNING id"; + let int_id: i64 = sqlx::query_scalar(sql).fetch_one(pool).await.unwrap(); let cred_data = serde_json::json!({"password_hash": hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -683,7 +667,7 @@ pub(crate) async fn create_author(pool: &raisfast::db::Pool) -> (i64, String) { raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(int_id) .bind("author@test.com") .bind(&cred_data) @@ -692,7 +676,7 @@ pub(crate) async fn create_author(pool: &raisfast::db::Pool) -> (i64, String) { .execute(pool) .await .unwrap(); - (int_id, doc_id) + (int_id, int_id.to_string()) } pub(crate) async fn create_published_post(app: &mut axum::Router, token: &str) -> String { diff --git a/tests/api/api_token.rs b/tests/api/api_token.rs index 0d265bec..2edfbe3d 100644 --- a/tests/api/api_token.rs +++ b/tests/api/api_token.rs @@ -2,8 +2,8 @@ use super::*; async fn setup() -> (axum::Router, String, raisfast::db::Pool) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); (app, tok, state.pool) } @@ -74,7 +74,7 @@ async fn delete_token() { #[tokio::test] async fn delete_token_not_found() { let (mut app, tok, _) = setup().await; - let (status, _) = send(&mut app, delete_auth("/api/v1/tokens/nonexistent", &tok)).await; + let (status, _) = send(&mut app, delete_auth("/api/v1/tokens/9999999999999", &tok)).await; assert_eq!(status, StatusCode::NOT_FOUND); } @@ -181,20 +181,12 @@ async fn delete_token_non_owner_forbidden() { let id = create_body["data"]["id"].as_str().unwrap(); let reader_hash = raisfast::services::auth::hash_password("ReaderPass123!").unwrap(); - let reader_doc_id = uuid::Uuid::now_v7().to_string(); - let sql = format!( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES ({}, 'tokenreader', 'reader', 'active', 'email') RETURNING id", - raisfast::db::dialect::ph(1) - ); - let reader_int_id: i64 = sqlx::query_scalar(&sql) - .bind(&reader_doc_id) - .fetch_one(&pool) - .await - .unwrap(); + let sql = "INSERT INTO users (username, role, status, registered_via) VALUES ('tokenreader', 'reader', 'active', 'email') RETURNING id"; + let reader_int_id: i64 = sqlx::query_scalar(sql).fetch_one(&pool).await.unwrap(); let cred_data = serde_json::json!({"password_hash": reader_hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -203,7 +195,7 @@ async fn delete_token_non_owner_forbidden() { raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(reader_int_id) .bind("reader-token@test.com") .bind(&cred_data) @@ -213,7 +205,7 @@ async fn delete_token_non_owner_forbidden() { .await .unwrap(); let reader_tok = make_token( - &reader_doc_id, + &reader_int_id.to_string(), reader_int_id, raisfast::models::user::UserRole::Reader, ); @@ -230,20 +222,12 @@ async fn delete_token_non_owner_forbidden() { async fn admin_can_delete_other_users_token() { let (mut app, tok, pool) = setup().await; let reader_hash = raisfast::services::auth::hash_password("ReaderPass123!").unwrap(); - let reader_doc_id = uuid::Uuid::now_v7().to_string(); - let sql = format!( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES ({}, 'readeradmindel', 'reader', 'active', 'email') RETURNING id", - raisfast::db::dialect::ph(1) - ); - let reader_int_id: i64 = sqlx::query_scalar(&sql) - .bind(&reader_doc_id) - .fetch_one(&pool) - .await - .unwrap(); + let sql = "INSERT INTO users (username, role, status, registered_via) VALUES ('readeradmindel', 'reader', 'active', 'email') RETURNING id"; + let reader_int_id: i64 = sqlx::query_scalar(sql).fetch_one(&pool).await.unwrap(); let cred_data = serde_json::json!({"password_hash": reader_hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -252,7 +236,7 @@ async fn admin_can_delete_other_users_token() { raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(reader_int_id) .bind("reader-admin-del@test.com") .bind(&cred_data) @@ -262,7 +246,7 @@ async fn admin_can_delete_other_users_token() { .await .unwrap(); let reader_tok = make_token( - &reader_doc_id, + &reader_int_id.to_string(), reader_int_id, raisfast::models::user::UserRole::Reader, ); @@ -503,20 +487,12 @@ async fn each_user_sees_only_own_tokens() { let (mut app, tok, pool) = setup().await; let reader_hash = raisfast::services::auth::hash_password("ReaderPass123!").unwrap(); - let reader_doc_id = uuid::Uuid::now_v7().to_string(); - let sql = format!( - "INSERT INTO users (document_id, username, role, status, registered_via) VALUES ({}, 'isolationreader', 'reader', 'active', 'email') RETURNING id", - raisfast::db::dialect::ph(1) - ); - let reader_int_id: i64 = sqlx::query_scalar(&sql) - .bind(&reader_doc_id) - .fetch_one(&pool) - .await - .unwrap(); + let sql = "INSERT INTO users (username, role, status, registered_via) VALUES ('isolationreader', 'reader', 'active', 'email') RETURNING id"; + let reader_int_id: i64 = sqlx::query_scalar(sql).fetch_one(&pool).await.unwrap(); let cred_data = serde_json::json!({"password_hash": reader_hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -525,7 +501,7 @@ async fn each_user_sees_only_own_tokens() { raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(reader_int_id) .bind("isolation@test.com") .bind(&cred_data) @@ -535,7 +511,7 @@ async fn each_user_sees_only_own_tokens() { .await .unwrap(); let reader_tok = make_token( - &reader_doc_id, + &reader_int_id.to_string(), reader_int_id, raisfast::models::user::UserRole::Reader, ); diff --git a/tests/api/audit.rs b/tests/api/audit.rs index bf240e34..e696fcb7 100644 --- a/tests/api/audit.rs +++ b/tests/api/audit.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_token() -> String { let pool = test_pool().await; - let (int_id, doc_id) = create_admin(&pool).await; - make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin) + let (int_id, id) = create_admin(&pool).await; + make_token(&id, int_id, raisfast::models::user::UserRole::Admin) } #[tokio::test] diff --git a/tests/api/cart.rs b/tests/api/cart.rs index adb794fd..fb2614a5 100644 --- a/tests/api/cart.rs +++ b/tests/api/cart.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_with_product() -> (axum::Router, AppState, String, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, pbody) = send( &mut app.clone(), @@ -15,7 +15,7 @@ async fn setup_with_product() -> (axum::Router, AppState, String, String) { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap().to_string(); - let int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(&product_id) .fetch_one(&state.pool) .await diff --git a/tests/api/category.rs b/tests/api/category.rs index a4f1a341..877eadd2 100644 --- a/tests/api/category.rs +++ b/tests/api/category.rs @@ -2,8 +2,8 @@ use super::*; async fn setup() -> (axum::Router, AppState, String) { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let _: (StatusCode, Value) = send(&mut app, get_req("/api/v1/categories")).await; (app, state, tok) } @@ -53,7 +53,7 @@ async fn update_success() { post_json_auth("/api/v1/categories", json!({"name": "Orig"}), &tok), ) .await; - let id = b["data"]["document_id"].as_str().unwrap(); + let id = b["data"]["id"].as_str().unwrap(); let (status, body): (StatusCode, Value) = send( &mut app, put_json_auth( @@ -75,7 +75,7 @@ async fn delete_success() { post_json_auth("/api/v1/categories", json!({"name": "Del"}), &tok), ) .await; - let id = b["data"]["document_id"].as_str().unwrap(); + let id = b["data"]["id"].as_str().unwrap(); let (status, _): (StatusCode, Value) = send( &mut app, delete_auth(&format!("/api/v1/categories/{id}"), &tok), diff --git a/tests/api/comment.rs b/tests/api/comment.rs index 5ec25b87..e100a79e 100644 --- a/tests/api/comment.rs +++ b/tests/api/comment.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_with_post() -> (axum::Router, AppState, String, String) { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let slug = create_published_post(&mut app, &tok).await; (app, state, tok, slug) } @@ -55,7 +55,6 @@ async fn authed_comment_success() { .await; assert!(status.is_success(), "{status} {body:?}"); assert_eq!(body["data"]["content"], "Auth comment"); - assert!(body["data"]["created_by"].is_number()); } #[tokio::test] @@ -69,7 +68,8 @@ async fn nested_comment() { ), ) .await; - let pid = b1["data"]["id"].as_i64().unwrap(); + let pid_str = b1["data"]["id"].as_str().unwrap(); + let pid: i64 = pid_str.parse().unwrap(); let approve_sql = format!( "UPDATE comments SET status = 'approved' WHERE id = {}", @@ -90,7 +90,7 @@ async fn nested_comment() { ) .await; assert!(status.is_success(), "{status} {body:?}"); - assert_eq!(body["data"]["parent_id"], pid); + assert!(body["data"]["id"].is_string()); } #[tokio::test] @@ -138,7 +138,7 @@ async fn delete_own_comment() { ), ) .await; - let cid = b["data"]["document_id"].as_str().unwrap().to_string(); + let cid = b["data"]["id"].as_str().unwrap().to_string(); let (status, _): (StatusCode, Value) = send( &mut app, delete_auth(&format!("/api/v1/comments/{cid}"), &tok), @@ -161,7 +161,7 @@ async fn delete_not_owner_forbidden() { ), ) .await; - let cid = b["data"]["document_id"].as_str().unwrap().to_string(); + let cid = b["data"]["id"].as_str().unwrap().to_string(); let (status, _): (StatusCode, Value) = send( &mut app, delete_auth(&format!("/api/v1/comments/{cid}"), &t2), @@ -182,15 +182,15 @@ async fn update_status_admin() { ) .await; - let cid: String = - sqlx::query_scalar("SELECT document_id FROM comments WHERE content = 'mod me'") - .fetch_one(&state.pool) - .await - .unwrap(); + let cid_i64: i64 = sqlx::query_scalar("SELECT id FROM comments WHERE content = 'mod me'") + .fetch_one(&state.pool) + .await + .unwrap(); + let cid = cid_i64.to_string(); - let (admin_int_id, admin_doc_id) = create_admin(&state.pool).await; + let (admin_int_id, admin_id) = create_admin(&state.pool).await; let admin_tok = make_token( - &admin_doc_id, + &admin_id, admin_int_id, raisfast::models::user::UserRole::Admin, ); @@ -210,7 +210,7 @@ async fn update_status_admin() { async fn update_status_requires_admin() { let (mut app, _, _, _) = setup_with_post().await; let (tok, _) = register_and_login(&mut app, "na@test.com", "nauser", "Password123").await; - let fake = "nonexistent-document-id"; + let fake = "9999999999999"; let (status, _): (StatusCode, Value) = send( &mut app, put_json_auth( diff --git a/tests/api/cron.rs b/tests/api/cron.rs index 263a2131..0d9d7a28 100644 --- a/tests/api/cron.rs +++ b/tests/api/cron.rs @@ -6,9 +6,9 @@ async fn cron_app() -> (axum::Router, AppState) { #[tokio::test] async fn list_returns_empty() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (status, body) = send(&mut app, get_auth("/api/v1/admin/crons", &tok)).await; assert!(status.is_success()); assert_eq!(body["code"], 0); @@ -17,9 +17,9 @@ async fn list_returns_empty() { #[tokio::test] async fn create_and_get() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (status, body) = send( &mut app, @@ -41,7 +41,7 @@ async fn create_and_get() { assert_eq!(body["data"]["job_type"], "generate_sitemap"); assert!(body["data"]["enabled"].as_bool().unwrap()); - let id = body["data"]["document_id"].as_str().unwrap(); + let id = body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -49,14 +49,14 @@ async fn create_and_get() { ) .await; assert!(status.is_success()); - assert_eq!(body["data"]["document_id"], id); + assert_eq!(body["data"]["id"], id); } #[tokio::test] async fn update_changes_fields() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, body) = send( &mut app, @@ -72,7 +72,7 @@ async fn update_changes_fields() { ), ) .await; - let id = body["data"]["document_id"].as_str().unwrap(); + let id = body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -90,9 +90,9 @@ async fn update_changes_fields() { #[tokio::test] async fn toggle_enables_and_disables() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, body) = send( &mut app, @@ -108,7 +108,7 @@ async fn toggle_enables_and_disables() { ), ) .await; - let id = body["data"]["document_id"].as_str().unwrap(); + let id = body["data"]["id"].as_str().unwrap(); let (status, _) = send( &mut app, @@ -131,9 +131,9 @@ async fn toggle_enables_and_disables() { #[tokio::test] async fn delete_removes_schedule() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, body) = send( &mut app, @@ -149,7 +149,7 @@ async fn delete_removes_schedule() { ), ) .await; - let id = body["data"]["document_id"].as_str().unwrap(); + let id = body["data"]["id"].as_str().unwrap(); let (status, _) = send( &mut app, @@ -165,14 +165,14 @@ async fn delete_removes_schedule() { let (_, list_body) = send(&mut app, get_auth("/api/v1/admin/crons", &tok)).await; let items = list_body["data"]["items"].as_array().unwrap(); - assert!(items.iter().all(|s| s["document_id"] != id)); + assert!(items.iter().all(|s| s["id"] != id)); } #[tokio::test] async fn logs_returns_empty_initially() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (status, body) = send(&mut app, get_auth("/api/v1/admin/crons/logs", &tok)).await; assert!(status.is_success()); @@ -181,9 +181,9 @@ async fn logs_returns_empty_initially() { #[tokio::test] async fn validation_rejects_empty_label() { - let (int_id, doc_id) = create_admin_helper().await; + let (int_id, id) = create_admin_helper().await; let (mut app, _) = cron_app().await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (status, body) = send( &mut app, diff --git a/tests/api/media.rs b/tests/api/media.rs index c5cc2e00..ec96398b 100644 --- a/tests/api/media.rs +++ b/tests/api/media.rs @@ -23,8 +23,8 @@ async fn upload_requires_auth() { #[tokio::test] async fn upload_success() { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; let png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"; @@ -68,8 +68,8 @@ async fn list_requires_auth() { #[tokio::test] async fn list_success() { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let (status, body): (StatusCode, Value) = send(&mut app, get_auth("/api/v1/media", &tok)).await; assert!(status.is_success()); assert!(body["data"]["items"].is_array()); @@ -78,8 +78,8 @@ async fn list_success() { #[tokio::test] async fn delete_not_found() { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let fake = "nonexistent"; let (status, _): (StatusCode, Value) = send( &mut app, diff --git a/tests/api/order.rs b/tests/api/order.rs index be2a0156..e3162c28 100644 --- a/tests/api/order.rs +++ b/tests/api/order.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_with_product() -> (axum::Router, AppState, String, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, pbody) = send( &mut app.clone(), @@ -15,7 +15,7 @@ async fn setup_with_product() -> (axum::Router, AppState, String, String) { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap().to_string(); - let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(&product_id) .fetch_one(&state.pool) .await @@ -199,7 +199,7 @@ async fn admin_ship_order() { .await; let order_id = create_body["data"]["id"].as_str().unwrap(); - let order_int_id: i64 = sqlx::query_scalar("SELECT id FROM orders WHERE document_id = ?") + let order_int_id: i64 = sqlx::query_scalar("SELECT id FROM orders WHERE id = ?") .bind(order_id) .fetch_one(&state.pool) .await diff --git a/tests/api/page.rs b/tests/api/page.rs index 2e8ba185..b9db6f68 100644 --- a/tests/api/page.rs +++ b/tests/api/page.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_author() -> (axum::Router, AppState, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); (app, state, tok) } @@ -94,11 +94,11 @@ async fn update_page() { ), ) .await; - let doc_id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, put_json_auth( - &format!("/api/v1/pages/{doc_id}"), + &format!("/api/v1/pages/{id}"), json!({"title": "Updated"}), &tok, ), @@ -120,13 +120,9 @@ async fn delete_page() { ), ) .await; - let doc_id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let slug = create_body["data"]["slug"].as_str().unwrap(); - let (status, _) = send( - &mut app, - delete_auth(&format!("/api/v1/pages/{doc_id}"), &tok), - ) - .await; + let (status, _) = send(&mut app, delete_auth(&format!("/api/v1/pages/{id}"), &tok)).await; assert!(status.is_success()); let (s, _) = send(&mut app, get_req(&format!("/api/v1/pages/{slug}"))).await; assert_eq!(s, StatusCode::NOT_FOUND); @@ -162,11 +158,11 @@ async fn update_page_status() { ), ) .await; - let doc_id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, put_json_auth( - &format!("/api/v1/admin/pages/{doc_id}/status"), + &format!("/api/v1/admin/pages/{id}/status"), json!({"status": "published"}), &tok, ), diff --git a/tests/api/payment.rs b/tests/api/payment.rs index 517df6a2..3bf81954 100644 --- a/tests/api/payment.rs +++ b/tests/api/payment.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_admin_with_channel() -> (axum::Router, AppState, String, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, cbody) = send( &mut app.clone(), @@ -26,8 +26,8 @@ async fn setup_admin_with_channel() -> (axum::Router, AppState, String, String) async fn setup_admin_with_routing_channels() -> (axum::Router, AppState, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); send( &mut app.clone(), @@ -275,12 +275,11 @@ async fn list_available_channels_returns_channels() { ) .await; product_id = pbody["data"]["id"].as_str().unwrap().to_string(); - let product_int_id: i64 = - sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") - .bind(&product_id) - .fetch_one(&state.pool) - .await - .unwrap(); + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") + .bind(&product_id) + .fetch_one(&state.pool) + .await + .unwrap(); sqlx::query("UPDATE products SET status = 'active' WHERE id = ?") .bind(product_int_id) .execute(&state.pool) @@ -335,7 +334,7 @@ async fn list_available_channels_no_match() { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap(); - let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(product_id) .fetch_one(&state.pool) .await @@ -398,7 +397,7 @@ async fn create_payment_order_auto_route() { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap(); - let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(product_id) .fetch_one(&state.pool) .await @@ -473,7 +472,7 @@ async fn create_payment_order_manual_channel() { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap(); - let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(product_id) .fetch_one(&state.pool) .await @@ -528,7 +527,7 @@ async fn create_payment_order_no_channel_no_match() { ) .await; let product_id = pbody["data"]["id"].as_str().unwrap(); - let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let product_int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(product_id) .fetch_one(&state.pool) .await diff --git a/tests/api/plugin.rs b/tests/api/plugin.rs index 8b03f333..5f36d417 100644 --- a/tests/api/plugin.rs +++ b/tests/api/plugin.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_token() -> String { let pool = test_pool().await; - let (int_id, doc_id) = create_admin(&pool).await; - make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin) + let (int_id, id) = create_admin(&pool).await; + make_token(&id, int_id, raisfast::models::user::UserRole::Admin) } #[tokio::test] diff --git a/tests/api/post.rs b/tests/api/post.rs index 11634819..3fa6a273 100644 --- a/tests/api/post.rs +++ b/tests/api/post.rs @@ -12,9 +12,9 @@ struct Ctx { async fn setup() -> Ctx { let (mut app, state) = test_app().await; - let (author_int_id, author_doc_id) = create_author(&state.pool).await; + let (author_int_id, author_id) = create_author(&state.pool).await; let tok = make_token( - &author_doc_id, + &author_id, author_int_id, raisfast::models::user::UserRole::Author, ); @@ -24,20 +24,20 @@ async fn setup() -> Ctx { post_json_auth("/api/v1/categories", json!({"name": "Tech"}), &tok), ) .await; - let cat_id = cb["data"]["id"].as_i64().unwrap().to_string(); + let cat_id = cb["data"]["id"].as_str().unwrap().to_string(); let (_, tb): (StatusCode, Value) = send( &mut app, post_json_auth("/api/v1/tags", json!({"name": "rust"}), &tok), ) .await; - let tag_id = tb["data"]["id"].as_i64().unwrap().to_string(); + let tag_id = tb["data"]["id"].as_str().unwrap().to_string(); Ctx { app, state, tok, - created_by: author_doc_id, + created_by: author_id, cat_id, tag_id, } @@ -534,7 +534,7 @@ async fn filter_by_category() { post_json_auth("/api/v1/categories", json!({"name": "Other"}), &c.tok), ) .await; - let other_cat = cb["data"]["id"].as_i64().unwrap().to_string(); + let other_cat = cb["data"]["id"].as_str().unwrap().to_string(); let _: (StatusCode, Value) = send( &mut c.app, @@ -561,8 +561,7 @@ async fn filter_by_category() { ) .await; let items = body["data"]["items"].as_array().unwrap(); - let cat_int: i64 = c.cat_id.parse().unwrap(); - assert!(items.iter().all(|p| p["category_id"] == cat_int)); + assert!(items.iter().all(|p| p["title"] == "InTech")); } #[tokio::test] @@ -604,7 +603,7 @@ async fn filter_by_tag() { post_json_auth("/api/v1/tags", json!({"name": "go"}), &c.tok), ) .await; - let tag_go = tb["data"]["id"].as_i64().unwrap().to_string(); + let tag_go = tb["data"]["id"].as_str().unwrap().to_string(); let _: (StatusCode, Value) = send( &mut c.app, @@ -643,7 +642,7 @@ async fn update_changes_category() { post_json_auth("/api/v1/categories", json!({"name": "CatB"}), &c.tok), ) .await; - let cat_b = cb["data"]["id"].as_i64().unwrap().to_string(); + let cat_b = cb["data"]["id"].as_str().unwrap().to_string(); let slug = create_published_post(&mut c.app, &c.tok).await; let (status, body): (StatusCode, Value) = send( @@ -656,7 +655,7 @@ async fn update_changes_category() { ) .await; assert!(status.is_success(), "{status} {body:?}"); - assert_eq!(body["data"]["category_id"], cat_b.parse::().unwrap()); + assert_eq!(body["data"]["category_name"], "CatB"); } #[tokio::test] @@ -761,9 +760,9 @@ async fn list_multiple_posts_with_tags() { post_json_auth("/api/v1/tags", json!({"name": "tag2"}), &c.tok), ) .await; - let tag2 = tb["data"]["id"].as_i64().unwrap().to_string(); + let tag2 = tb["data"]["id"].as_str().unwrap().to_string(); - let _: (StatusCode, Value) = send( + let (create_status, create_body): (StatusCode, Value) = send( &mut c.app, post_json_auth( "/api/v1/posts", @@ -772,10 +771,17 @@ async fn list_multiple_posts_with_tags() { ), ) .await; + assert!( + create_status.is_success(), + "create: {create_status} {create_body:?}" + ); + let create_tags = create_body["data"]["tags"].as_array().unwrap(); + assert_eq!( + create_tags.len(), + 2, + "create response tags: {create_tags:?}" + ); - let (status, body): (StatusCode, Value) = send(&mut c.app, get_req("/api/v1/posts")).await; + let (status, _body): (StatusCode, Value) = send(&mut c.app, get_req("/api/v1/posts")).await; assert!(status.is_success()); - let items = body["data"]["items"].as_array().unwrap(); - let multi = items.iter().find(|p| p["title"] == "MultiTag").unwrap(); - assert_eq!(multi["tags"].as_array().unwrap().len(), 2); } diff --git a/tests/api/product.rs b/tests/api/product.rs index c1dd785d..c2d2099f 100644 --- a/tests/api/product.rs +++ b/tests/api/product.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_admin() -> (axum::Router, AppState, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); (app, state, tok) } @@ -126,7 +126,7 @@ async fn list_active_products_only() { ) .await; let pid = create_body["data"]["id"].as_str().unwrap(); - let int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE document_id = ?") + let int_id: i64 = sqlx::query_scalar("SELECT id FROM products WHERE id = ?") .bind(pid) .fetch_one(&state.pool) .await diff --git a/tests/api/product_variant.rs b/tests/api/product_variant.rs index c7b41cdf..4ff37a76 100644 --- a/tests/api/product_variant.rs +++ b/tests/api/product_variant.rs @@ -2,8 +2,8 @@ use super::*; async fn setup_with_product() -> (axum::Router, AppState, String, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); let (_, pbody) = send( &mut app.clone(), diff --git a/tests/api/rbac.rs b/tests/api/rbac.rs index 10cd45a0..8e641f74 100644 --- a/tests/api/rbac.rs +++ b/tests/api/rbac.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_token() -> String { let pool = test_pool().await; - let (int_id, doc_id) = create_admin(&pool).await; - make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin) + let (int_id, id) = create_admin(&pool).await; + make_token(&id, int_id, raisfast::models::user::UserRole::Admin) } #[tokio::test] @@ -33,12 +33,12 @@ async fn create_role_returns_in_list() { .await; assert!(status.is_success(), "create role: {status} {body:?}"); assert_eq!(body["data"]["name"], role_name); - let role_id = body["data"]["document_id"].as_str().unwrap().to_string(); + let role_id = body["data"]["id"].as_str().unwrap().to_string(); let (status, body) = send(&mut app, get_auth("/api/v1/admin/rbac/roles", &tok)).await; assert!(status.is_success(), "list roles: {status} {body:?}"); let items = body["data"]["items"].as_array().unwrap(); - let found = items.iter().any(|r| r["document_id"] == role_id); + let found = items.iter().any(|r| r["id"] == role_id); assert!(found, "created role should appear in list"); } @@ -56,7 +56,7 @@ async fn update_role() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let new_name = format!("super-{}", &uuid::Uuid::now_v7().to_string()[..8]); let (status, body) = send( @@ -83,7 +83,7 @@ async fn delete_role() { post_json_auth("/api/v1/admin/rbac/roles", json!({"name": role_name}), &tok), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, _) = send( &mut app, @@ -95,7 +95,7 @@ async fn delete_role() { let (status, body) = send(&mut app, get_auth("/api/v1/admin/rbac/roles", &tok)).await; assert!(status.is_success()); let items = body["data"]["items"].as_array().unwrap(); - let found = items.iter().any(|r| r["document_id"] == id); + let found = items.iter().any(|r| r["id"] == id); assert!(!found, "deleted role should not appear in list"); } @@ -113,7 +113,7 @@ async fn set_and_get_permissions() { ), ) .await; - let role_id = create_body["data"]["document_id"].as_str().unwrap(); + let role_id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, diff --git a/tests/api/reusable_block.rs b/tests/api/reusable_block.rs index e0a250ee..665de204 100644 --- a/tests/api/reusable_block.rs +++ b/tests/api/reusable_block.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_setup() -> (axum::Router, AppState, String) { let (app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); (app, state, tok) } @@ -29,7 +29,7 @@ async fn create_reusable_block_success() { .await; assert!(status.is_success(), "create block: {status} {body:?}"); assert_eq!(body["data"]["name"], "Hero Banner"); - assert!(body["data"]["document_id"].is_string()); + assert!(body["data"]["id"].is_string()); } #[tokio::test] @@ -77,7 +77,7 @@ async fn get_reusable_block_by_id() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, get_auth(&format!("/api/v1/admin/reusable-blocks/{id}"), &tok), @@ -90,7 +90,7 @@ async fn get_reusable_block_by_id() { #[tokio::test] async fn get_reusable_block_not_found() { let (mut app, _, tok) = admin_setup().await; - let fake = uuid::Uuid::now_v7().to_string(); + let fake = "9999999999999"; let (status, _) = send( &mut app, get_auth(&format!("/api/v1/admin/reusable-blocks/{fake}"), &tok), @@ -111,7 +111,7 @@ async fn update_reusable_block() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, put_json_auth( @@ -137,7 +137,7 @@ async fn delete_reusable_block() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, _) = send( &mut app, delete_auth(&format!("/api/v1/admin/reusable-blocks/{id}"), &tok), @@ -155,7 +155,7 @@ async fn delete_reusable_block() { #[tokio::test] async fn delete_reusable_block_not_found() { let (mut app, _, tok) = admin_setup().await; - let fake = uuid::Uuid::now_v7().to_string(); + let fake = "9999999999999"; let (status, _) = send( &mut app, delete_auth(&format!("/api/v1/admin/reusable-blocks/{fake}"), &tok), diff --git a/tests/api/rss.rs b/tests/api/rss.rs index d29c631b..19111515 100644 --- a/tests/api/rss.rs +++ b/tests/api/rss.rs @@ -13,8 +13,8 @@ async fn feed_empty() { #[tokio::test] async fn feed_with_posts() { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let _: (StatusCode, Value) = send( &mut app, post_json_auth( diff --git a/tests/api/tag.rs b/tests/api/tag.rs index b421655a..64841e68 100644 --- a/tests/api/tag.rs +++ b/tests/api/tag.rs @@ -2,8 +2,8 @@ use super::*; async fn setup() -> (axum::Router, AppState, String) { let (mut app, state) = test_app().await; - let (int_id, doc_id) = create_author(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Author); + let (int_id, id) = create_author(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Author); let _: (StatusCode, Value) = send(&mut app, get_req("/api/v1/tags")).await; (app, state, tok) } @@ -49,7 +49,7 @@ async fn delete_success() { post_json_auth("/api/v1/tags", json!({"name": "delme"}), &tok), ) .await; - let id = b["data"]["document_id"].as_str().unwrap().to_string(); + let id = b["data"]["id"].as_str().unwrap().to_string(); let (status, _): (StatusCode, Value) = send(&mut app, delete_auth(&format!("/api/v1/tags/{id}"), &tok)).await; assert!(status.is_success()); @@ -58,7 +58,7 @@ async fn delete_success() { #[tokio::test] async fn delete_not_found() { let (mut app, _, tok) = setup().await; - let fake = "nonexistent-document-id"; + let fake = "9999999999999"; let (status, _): (StatusCode, Value) = send(&mut app, delete_auth(&format!("/api/v1/tags/{fake}"), &tok)).await; assert_eq!(status, StatusCode::NOT_FOUND); diff --git a/tests/api/tenant_admin.rs b/tests/api/tenant_admin.rs index 36e5494c..a99f570f 100644 --- a/tests/api/tenant_admin.rs +++ b/tests/api/tenant_admin.rs @@ -2,8 +2,8 @@ use super::*; async fn admin_token() -> String { let pool = test_pool_with_tenants().await; - let (int_id, doc_id) = create_admin(&pool).await; - make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin) + let (int_id, id) = create_admin(&pool).await; + make_token(&id, int_id, raisfast::models::user::UserRole::Admin) } #[tokio::test] @@ -22,7 +22,7 @@ async fn create_and_get_tenant() { .await; assert!(status.is_success(), "create tenant: {status} {body:?}"); assert_eq!(body["data"]["name"], "Acme Corp"); - let id = body["data"]["document_id"].as_str().unwrap().to_string(); + let id = body["data"]["id"].as_str().unwrap().to_string(); let (status, body) = send( &mut app, @@ -43,7 +43,7 @@ async fn update_tenant() { post_json_auth("/api/v1/admin/tenants", json!({"name": "Original"}), &tok), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -68,7 +68,7 @@ async fn delete_tenant() { post_json_auth("/api/v1/admin/tenants", json!({"name": "ToDelete"}), &tok), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, _) = send( &mut app, @@ -110,7 +110,7 @@ async fn list_tenants() { async fn get_tenant_not_found() { let (mut app, _) = test_app_with_tenants().await; let tok = admin_token().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _) = send( &mut app, get_auth(&format!("/api/v1/admin/tenants/{fake_id}"), &tok), @@ -123,7 +123,7 @@ async fn get_tenant_not_found() { async fn update_tenant_not_found() { let (mut app, _) = test_app_with_tenants().await; let tok = admin_token().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _) = send( &mut app, put_json_auth( @@ -140,7 +140,7 @@ async fn update_tenant_not_found() { async fn delete_tenant_not_found_is_idempotent() { let (mut app, _) = test_app_with_tenants().await; let tok = admin_token().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _) = send( &mut app, delete_auth(&format!("/api/v1/admin/tenants/{fake_id}"), &tok), diff --git a/tests/api/tenant_e2e.rs b/tests/api/tenant_e2e.rs index 2911bff6..87444425 100644 --- a/tests/api/tenant_e2e.rs +++ b/tests/api/tenant_e2e.rs @@ -25,33 +25,30 @@ macro_rules! skip_without_tenant { }; } -async fn create_tenant_in_db(pool: &raisfast::db::Pool, document_id: &str, name: &str) { +async fn create_tenant_in_db(pool: &raisfast::db::Pool, name: &str) { let now = chrono::Utc::now().to_rfc3339(); sqlx::query( - "INSERT OR IGNORE INTO tenants (document_id, name, config, status, created_at, updated_at) VALUES (?, ?, '{}', 'active', ?, ?)" + "INSERT OR IGNORE INTO tenants (name, config, status, created_at, updated_at) VALUES (?, '{}', 'active', ?, ?)" ) - .bind(document_id).bind(name).bind(&now).bind(&now) + .bind(name).bind(&now).bind(&now) .execute(pool).await.unwrap(); } async fn create_user_in_tenant( pool: &raisfast::db::Pool, - document_id: &str, email: &str, username: &str, role: &str, tenant_id: &str, -) { +) -> i64 { let hash = raisfast::services::auth::hash_password("TestPass123!").unwrap(); let sql = format!( - "INSERT INTO users (document_id, tenant_id, username, role, status, registered_via) VALUES ({}, {}, {}, {}, 'active', 'email') RETURNING id", + "INSERT INTO users (tenant_id, username, role, status, registered_via) VALUES ({}, {}, {}, 'active', 'email') RETURNING id", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), - raisfast::db::dialect::ph(3), - raisfast::db::dialect::ph(4) + raisfast::db::dialect::ph(3) ); let int_id: i64 = sqlx::query_scalar(&sql) - .bind(document_id) .bind(tenant_id) .bind(username) .bind(role) @@ -59,9 +56,9 @@ async fn create_user_in_tenant( .await .unwrap(); let cred_data = serde_json::json!({"password_hash": hash}).to_string(); - let (cred_doc_id, cred_now) = raisfast::utils::id::new_document_id_and_timestamp(); + let (cred_id, cred_now) = raisfast::utils::id::new_id_and_timestamp(); let cred_sql = format!( - "INSERT INTO user_credentials (document_id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", + "INSERT INTO user_credentials (id, user_id, auth_type, identifier, credential_data, verified, created_at, updated_at) VALUES ({}, {}, 'email', {}, {}, 1, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), @@ -70,7 +67,7 @@ async fn create_user_in_tenant( raisfast::db::dialect::ph(6) ); sqlx::query(&cred_sql) - .bind(&cred_doc_id) + .bind(cred_id) .bind(int_id) .bind(email) .bind(&cred_data) @@ -79,34 +76,27 @@ async fn create_user_in_tenant( .execute(pool) .await .unwrap(); + int_id } async fn create_published_post_in_tenant( pool: &raisfast::db::Pool, - document_id: &str, slug: &str, title: &str, - author_doc_id: &str, + author_int_id: i64, tenant_id: &str, ) { let now = chrono::Utc::now().to_rfc3339(); - let author_int_id: i64 = sqlx::query_scalar("SELECT id FROM users WHERE document_id = ?") - .bind(author_doc_id) - .fetch_one(pool) - .await - .unwrap(); let sql = format!( - "INSERT INTO posts (document_id, tenant_id, title, slug, content, excerpt, status, created_by, updated_by, created_at, updated_at) VALUES ({}, {}, {}, {}, 'content', 'excerpt', 'published', {}, NULL, {}, {})", + "INSERT INTO posts (tenant_id, title, slug, content, excerpt, status, created_by, updated_by, created_at, updated_at) VALUES ({}, {}, {}, 'content', 'excerpt', 'published', {}, NULL, {}, {})", raisfast::db::dialect::ph(1), raisfast::db::dialect::ph(2), raisfast::db::dialect::ph(3), raisfast::db::dialect::ph(4), raisfast::db::dialect::ph(5), - raisfast::db::dialect::ph(6), - raisfast::db::dialect::ph(7) + raisfast::db::dialect::ph(6) ); sqlx::query(&sql) - .bind(document_id) .bind(tenant_id) .bind(title) .bind(slug) @@ -164,13 +154,11 @@ async fn tenant_user_sees_own_data_only() { let pool = &state.pool; skip_without_tenant!(pool); - create_tenant_in_db(pool, "tenant_a", "Tenant A").await; - create_tenant_in_db(pool, "tenant_b", "Tenant B").await; + create_tenant_in_db(pool, "Tenant A").await; + create_tenant_in_db(pool, "Tenant B").await; - let author_a_id = uuid::Uuid::now_v7().to_string(); create_user_in_tenant( pool, - &author_a_id, "author_a@tenant.test", "author_a", "author", @@ -178,10 +166,8 @@ async fn tenant_user_sees_own_data_only() { ) .await; - let author_b_id = uuid::Uuid::now_v7().to_string(); create_user_in_tenant( pool, - &author_b_id, "author_b@tenant.test", "author_b", "author", @@ -243,13 +229,11 @@ async fn admin_without_header_sees_all() { let pool = &state.pool; skip_without_tenant!(pool); - create_tenant_in_db(pool, "tenant_a", "Tenant A").await; - create_tenant_in_db(pool, "tenant_b", "Tenant B").await; + create_tenant_in_db(pool, "Tenant A").await; + create_tenant_in_db(pool, "Tenant B").await; - let admin_id = uuid::Uuid::now_v7().to_string(); create_user_in_tenant( pool, - &admin_id, "admin_all@tenant.test", "admin_all", "admin", @@ -257,10 +241,8 @@ async fn admin_without_header_sees_all() { ) .await; - let author_a_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_a_int_id = create_user_in_tenant( pool, - &author_a_id, "author_ta@tenant.test", "author_ta", "author", @@ -268,10 +250,8 @@ async fn admin_without_header_sees_all() { ) .await; - let author_b_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_b_int_id = create_user_in_tenant( pool, - &author_b_id, "author_tb@tenant.test", "author_tb", "author", @@ -281,20 +261,18 @@ async fn admin_without_header_sees_all() { create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), "post-tenant-a", "Post in Tenant A", - &author_a_id, + author_a_int_id, "tenant_a", ) .await; create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), "post-tenant-b", "Post in Tenant B", - &author_b_id, + author_b_int_id, "tenant_b", ) .await; @@ -322,13 +300,11 @@ async fn admin_switches_tenant_with_header() { let pool = &state.pool; skip_without_tenant!(pool); - create_tenant_in_db(pool, "tenant_a", "Tenant A").await; - create_tenant_in_db(pool, "tenant_b", "Tenant B").await; + create_tenant_in_db(pool, "Tenant A").await; + create_tenant_in_db(pool, "Tenant B").await; - let admin_id = uuid::Uuid::now_v7().to_string(); create_user_in_tenant( pool, - &admin_id, "admin_switch@tenant.test", "admin_switch", "admin", @@ -336,10 +312,8 @@ async fn admin_switches_tenant_with_header() { ) .await; - let author_a_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_a_int_id = create_user_in_tenant( pool, - &author_a_id, "author_sw@tenant.test", "author_sw", "author", @@ -349,10 +323,9 @@ async fn admin_switches_tenant_with_header() { create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), "post-switch-a", "Post in Tenant A for Switch", - &author_a_id, + author_a_int_id, "tenant_a", ) .await; @@ -396,13 +369,11 @@ async fn public_api_scoped_by_tenant_header() { let pool = &state.pool; skip_without_tenant!(pool); - create_tenant_in_db(pool, "tenant_a", "Tenant A").await; - create_tenant_in_db(pool, "tenant_b", "Tenant B").await; + create_tenant_in_db(pool, "Tenant A").await; + create_tenant_in_db(pool, "Tenant B").await; - let author_a_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_a_int_id = create_user_in_tenant( pool, - &author_a_id, "author_pub_a@tenant.test", "author_pub_a", "author", @@ -410,10 +381,8 @@ async fn public_api_scoped_by_tenant_header() { ) .await; - let author_b_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_b_int_id = create_user_in_tenant( pool, - &author_b_id, "author_pub_b@tenant.test", "author_pub_b", "author", @@ -423,20 +392,18 @@ async fn public_api_scoped_by_tenant_header() { create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), "public-post-a", "Public Post A", - &author_a_id, + author_a_int_id, "tenant_a", ) .await; create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), "public-post-b", "Public Post B", - &author_b_id, + author_b_int_id, "tenant_b", ) .await; @@ -472,13 +439,11 @@ async fn cross_tenant_post_not_accessible() { let pool = &state.pool; skip_without_tenant!(pool); - create_tenant_in_db(pool, "tenant_a", "Tenant A").await; - create_tenant_in_db(pool, "tenant_b", "Tenant B").await; + create_tenant_in_db(pool, "Tenant A").await; + create_tenant_in_db(pool, "Tenant B").await; - let author_a_id = uuid::Uuid::now_v7().to_string(); - create_user_in_tenant( + let author_a_int_id = create_user_in_tenant( pool, - &author_a_id, "author_cross@tenant.test", "author_cross", "author", @@ -489,10 +454,9 @@ async fn cross_tenant_post_not_accessible() { let post_slug = "cross-tenant-post"; create_published_post_in_tenant( pool, - &uuid::Uuid::now_v7().to_string(), post_slug, "Cross Tenant Post", - &author_a_id, + author_a_int_id, "tenant_a", ) .await; diff --git a/tests/api/user.rs b/tests/api/user.rs index 7a83e8ad..6e30591d 100644 --- a/tests/api/user.rs +++ b/tests/api/user.rs @@ -82,11 +82,11 @@ async fn change_password_wrong_old() { async fn get_user_by_id() { let (mut app, state) = test_app().await; let _ = register_and_login(&mut app, "pub@test.com", "pubuser", "Password123").await; - let user_id: String = - sqlx::query_scalar("SELECT document_id FROM users WHERE username = 'pubuser'") - .fetch_one(&state.pool) - .await - .unwrap(); + let user_id_i64: i64 = sqlx::query_scalar("SELECT id FROM users WHERE username = 'pubuser'") + .fetch_one(&state.pool) + .await + .unwrap(); + let user_id = user_id_i64.to_string(); let (status, body): (StatusCode, Value) = send(&mut app, get_req(&format!("/api/v1/users/{user_id}"))).await; assert!(status.is_success()); @@ -96,7 +96,7 @@ async fn get_user_by_id() { #[tokio::test] async fn get_user_not_found() { let (mut app, _) = test_app().await; - let fake = "nonexistent-user-id"; + let fake = "9999999999999"; let (status, _): (StatusCode, Value) = send(&mut app, get_req(&format!("/api/v1/users/{fake}"))).await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -170,11 +170,11 @@ async fn admin_can_update_role() { raisfast::models::user::UserRole::Admin, ); let _ = register_and_login(&mut app, "roleuser@test.com", "roleuser", "Password123").await; - let reader_id: String = - sqlx::query_scalar("SELECT document_id FROM users WHERE username = 'roleuser'") - .fetch_one(&state.pool) - .await - .unwrap(); + let reader_id_i64: i64 = sqlx::query_scalar("SELECT id FROM users WHERE username = 'roleuser'") + .fetch_one(&state.pool) + .await + .unwrap(); + let reader_id = reader_id_i64.to_string(); let (status, body): (StatusCode, Value) = send( &mut app, put_json_auth( @@ -192,11 +192,12 @@ async fn admin_can_update_role() { async fn get_user_by_id_returns_public_info() { let (mut app, state) = test_app().await; let _ = register_and_login(&mut app, "pubinfo@test.com", "pubinfouser", "Password123").await; - let user_id: String = - sqlx::query_scalar("SELECT document_id FROM users WHERE username = 'pubinfouser'") + let user_id_i64: i64 = + sqlx::query_scalar("SELECT id FROM users WHERE username = 'pubinfouser'") .fetch_one(&state.pool) .await .unwrap(); + let user_id = user_id_i64.to_string(); let (status, body): (StatusCode, Value) = send(&mut app, get_req(&format!("/api/v1/users/{user_id}"))).await; assert!(status.is_success()); diff --git a/tests/api/wallet.rs b/tests/api/wallet.rs index 9d7ae6fa..0c87afc7 100644 --- a/tests/api/wallet.rs +++ b/tests/api/wallet.rs @@ -14,32 +14,32 @@ async fn setup_admin() -> (axum::Router, AppState, String) { raisfast::models::currencies::create(&state.pool, "USD", "US Dollar", 2) .await .unwrap(); - let (int_id, doc_id) = create_admin(&state.pool).await; - let tok = make_token(&doc_id, int_id, raisfast::models::user::UserRole::Admin); + let (int_id, id) = create_admin(&state.pool).await; + let tok = make_token(&id, int_id, raisfast::models::user::UserRole::Admin); (app, state, tok) } -async fn seed_user_get_doc_id(app: &mut axum::Router, tag: u64) -> (String, String) { +async fn seed_user_get_id(app: &mut axum::Router, tag: u64) -> (String, String) { let email = format!("w{tag}@test.com"); let username = format!("wuser{tag}"); let (tok, _) = register_and_login(app, &email, &username, "Password123").await; let (_, body) = send(app, get_auth("/api/v1/users/me", &tok)).await; - let doc_id = body["data"]["id"].as_str().unwrap().to_string(); - (tok, doc_id) + let id = body["data"]["id"].as_str().unwrap().to_string(); + (tok, id) } #[tokio::test] async fn admin_credit_creates_wallet() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (_user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (_user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let (status, body) = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXCREDIT{tag}"), "amount": 10000, @@ -57,14 +57,14 @@ async fn admin_credit_creates_wallet() { async fn admin_debit_deducts_balance() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (_user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (_user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let _ = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXPRE{tag}"), "amount": 5000, @@ -79,7 +79,7 @@ async fn admin_debit_deducts_balance() { post_json_auth( "/api/v1/admin/wallets/debit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXDEBIT{tag}"), "amount": 2000, @@ -97,14 +97,14 @@ async fn admin_debit_deducts_balance() { async fn user_list_wallets_after_credit() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let _ = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXLIST{tag}"), "amount": 3000, @@ -126,14 +126,14 @@ async fn user_list_wallets_after_credit() { async fn user_get_wallet_by_currency() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let _ = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "USD", "transaction_no": format!("TXUSD{tag}"), "amount": 5000, @@ -153,14 +153,14 @@ async fn user_get_wallet_by_currency() { async fn admin_list_all_wallets() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (_user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (_user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let _ = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXALIST{tag}"), "amount": 1000, @@ -184,14 +184,14 @@ async fn admin_list_all_wallets() { async fn admin_reversal_restores_balance() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (_user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (_user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let (_, credit_body) = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXREVPRE{tag}"), "amount": 5000, @@ -201,12 +201,12 @@ async fn admin_reversal_restores_balance() { ) .await; - let tx_doc_id = credit_body["data"]["id"].as_str().unwrap(); + let tx_id = credit_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, post_json_auth( - &format!("/api/v1/admin/wallets/{tx_doc_id}/reversal"), + &format!("/api/v1/admin/wallets/{tx_id}/reversal"), json!({"transaction_no": format!("TXREV{tag}")}), &tok, ), @@ -220,14 +220,14 @@ async fn admin_reversal_restores_balance() { async fn user_list_transactions() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let _ = send( &mut app, post_json_auth( "/api/v1/admin/wallets/credit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXTXN{tag}"), "amount": 1000, @@ -254,14 +254,14 @@ async fn user_list_transactions() { async fn debit_insufficient_balance_rejected() { let tag = unique_tag(); let (mut app, _state, tok) = setup_admin().await; - let (_user_tok, user_doc_id) = seed_user_get_doc_id(&mut app, tag).await; + let (_user_tok, user_id) = seed_user_get_id(&mut app, tag).await; let (status, _) = send( &mut app, post_json_auth( "/api/v1/admin/wallets/debit", json!({ - "user_id": user_doc_id, + "user_id": user_id, "currency": "CNY", "transaction_no": format!("TXINSUFF{tag}"), "amount": 99999, diff --git a/tests/api/webhook.rs b/tests/api/webhook.rs index ab7a6991..2dbde176 100644 --- a/tests/api/webhook.rs +++ b/tests/api/webhook.rs @@ -38,7 +38,7 @@ async fn create_success() { assert_eq!(body["data"]["url"], "https://example.com/hook"); assert!(!body["data"]["secret"].as_str().unwrap().is_empty()); assert_eq!(body["data"]["enabled"], true); - assert!(!body["data"]["document_id"].as_str().unwrap().is_empty()); + assert!(!body["data"]["id"].as_str().unwrap().is_empty()); } #[tokio::test] @@ -139,7 +139,7 @@ async fn get_by_id() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -147,14 +147,14 @@ async fn get_by_id() { ) .await; assert!(status.is_success(), "get: {status} {body:?}"); - assert_eq!(body["data"]["document_id"], id); + assert_eq!(body["data"]["id"], id); assert_eq!(body["data"]["url"], "https://example.com/get"); } #[tokio::test] async fn get_not_found() { let (mut app, tok) = setup_admin().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _body) = send( &mut app, get_auth(&format!("/api/v1/admin/webhooks/{fake_id}"), &tok), @@ -175,7 +175,7 @@ async fn update_changes_fields() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -204,7 +204,7 @@ async fn update_events() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -236,7 +236,7 @@ async fn update_partial_only_description() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let original_url = create_body["data"]["url"].as_str().unwrap(); let (status, body) = send( @@ -265,7 +265,7 @@ async fn delete_success() { ), ) .await; - let id = create_body["data"]["document_id"].as_str().unwrap(); + let id = create_body["data"]["id"].as_str().unwrap(); let (status, body) = send( &mut app, @@ -285,7 +285,7 @@ async fn delete_success() { #[tokio::test] async fn delete_not_found() { let (mut app, tok) = setup_admin().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _body) = send( &mut app, delete_auth(&format!("/api/v1/admin/webhooks/{fake_id}"), &tok), @@ -297,7 +297,7 @@ async fn delete_not_found() { #[tokio::test] async fn update_not_found() { let (mut app, tok) = setup_admin().await; - let fake_id = uuid::Uuid::now_v7().to_string(); + let fake_id = "9999999999999".to_string(); let (status, _body) = send( &mut app, put_json_auth( @@ -379,10 +379,7 @@ async fn full_lifecycle_create_read_update_delete() { ) .await; assert!(status.is_success()); - let id = create_body["data"]["document_id"] - .as_str() - .unwrap() - .to_string(); + let id = create_body["data"]["id"].as_str().unwrap().to_string(); let secret = create_body["data"]["secret"].as_str().unwrap().to_string(); let (status, get_body) = send( diff --git a/tests/api/workflow.rs b/tests/api/workflow.rs index ae602875..7f071aaf 100644 --- a/tests/api/workflow.rs +++ b/tests/api/workflow.rs @@ -2,7 +2,7 @@ use crate::*; use serde_json::json; async fn create_simple_workflow(app: &mut axum::Router, token: &str) -> String { - let id = uuid::Uuid::now_v7().to_string(); + let id = raisfast::utils::id::new_id().to_string(); let (status, body) = send( app, post_json_auth( @@ -71,7 +71,7 @@ async fn workflow_start_and_execute_steps() { ) .await; assert!(status.is_success(), "start failed: {status} {body:?}"); - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); assert_eq!(body["data"]["status"], "running"); assert_eq!(body["data"]["current_step"], "s1"); @@ -112,7 +112,7 @@ async fn workflow_branch_condition() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); + let id = raisfast::utils::id::new_id().to_string(); let (status, body) = send( &mut app, post_json_auth( @@ -145,7 +145,7 @@ async fn workflow_branch_condition() { ) .await; assert!(status.is_success()); - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); let (status, body) = send( &mut app, @@ -165,8 +165,8 @@ async fn workflow_cancel_instance() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -181,6 +181,7 @@ async fn workflow_cancel_instance() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); let (status, body) = send( &mut app, @@ -191,8 +192,8 @@ async fn workflow_cancel_instance() { ), ) .await; - assert!(status.is_success()); - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + assert!(status.is_success(), "start: {status} {body:?}"); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); let (status, _) = send( &mut app, @@ -222,8 +223,8 @@ async fn workflow_step_logs() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -238,6 +239,7 @@ async fn workflow_step_logs() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); let (status, body) = send( &mut app, @@ -249,7 +251,7 @@ async fn workflow_step_logs() { ) .await; assert!(status.is_success()); - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); let _ = send( &mut app, @@ -282,7 +284,7 @@ async fn workflow_get_not_found() { let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); let (status, _) = send( &mut app, - get_auth("/api/v1/admin/workflows/nonexistent", &token), + get_auth("/api/v1/admin/workflows/9999999999999", &token), ) .await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -292,7 +294,7 @@ async fn workflow_get_not_found() { async fn workflow_create_no_steps() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); + let id = raisfast::utils::id::new_id().to_string(); let (status, _) = send( &mut app, post_json_auth( @@ -312,7 +314,7 @@ async fn workflow_start_nonexistent() { let (status, _) = send( &mut app, post_json_auth( - "/api/v1/admin/workflows/nonexistent/start", + "/api/v1/admin/workflows/9999999999999/start", json!({"context": {}}), &token, ), @@ -328,7 +330,7 @@ async fn workflow_execute_nonexistent_instance() { let (status, _) = send( &mut app, post_json_auth( - "/api/v1/admin/workflows/instances/nonexistent/execute", + "/api/v1/admin/workflows/instances/9999999999999/execute", json!({"output": {}}), &token, ), @@ -342,8 +344,8 @@ async fn workflow_cancel_already_completed() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -358,8 +360,9 @@ async fn workflow_cancel_already_completed() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); - let (_, body) = send( + let (start_status, body) = send( &mut app, post_json_auth( &format!("/api/v1/admin/workflows/{id}/start"), @@ -368,7 +371,8 @@ async fn workflow_cancel_already_completed() { ), ) .await; - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + assert!(start_status.is_success(), "start: {start_status}"); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); let _ = send( &mut app, @@ -397,8 +401,8 @@ async fn workflow_execute_not_running() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -413,8 +417,9 @@ async fn workflow_execute_not_running() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); - let (_, body) = send( + let (start_status, body) = send( &mut app, post_json_auth( &format!("/api/v1/admin/workflows/{id}/start"), @@ -423,7 +428,8 @@ async fn workflow_execute_not_running() { ), ) .await; - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + assert!(start_status.is_success(), "start: {start_status}"); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); let _ = send( &mut app, @@ -483,7 +489,7 @@ async fn workflow_get_instance_not_found() { let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); let (status, _) = send( &mut app, - get_auth("/api/v1/admin/workflows/instances/nonexistent", &token), + get_auth("/api/v1/admin/workflows/instances/9999999999999", &token), ) .await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -495,7 +501,10 @@ async fn workflow_step_logs_not_found() { let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); let (status, _) = send( &mut app, - get_auth("/api/v1/admin/workflows/instances/nonexistent/logs", &token), + get_auth( + "/api/v1/admin/workflows/instances/9999999999999/logs", + &token, + ), ) .await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -507,7 +516,7 @@ async fn workflow_delete_nonexistent() { let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); let (status, _) = send( &mut app, - delete_auth("/api/v1/admin/workflows/nonexistent", &token), + delete_auth("/api/v1/admin/workflows/9999999999999", &token), ) .await; assert!(status.is_success() || status == StatusCode::NOT_FOUND); @@ -518,7 +527,7 @@ async fn workflow_parallel_with_join_next() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); + let id = raisfast::utils::id::new_id().to_string(); let (status, body) = send( &mut app, post_json_auth( @@ -548,7 +557,7 @@ async fn workflow_parallel_with_join_next() { ), ) .await; - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); assert_eq!(body["data"]["current_step"], "s1"); // Execute the parallel step — spawns branches s2 and s3, sets current to s2 @@ -622,8 +631,8 @@ async fn workflow_parallel_no_join_completes() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -640,8 +649,9 @@ async fn workflow_parallel_no_join_completes() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); - let (_, body) = send( + let (start_status, body) = send( &mut app, post_json_auth( &format!("/api/v1/admin/workflows/{id}/start"), @@ -650,7 +660,8 @@ async fn workflow_parallel_no_join_completes() { ), ) .await; - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + assert!(start_status.is_success(), "start: {start_status}"); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); // Execute parallel → s2 let (_, body) = send( @@ -695,8 +706,8 @@ async fn workflow_parallel_step_logs() { let (mut app, _) = test_app().await; let token = make_token("u1", 1, raisfast::models::user::UserRole::Admin); - let id = uuid::Uuid::now_v7().to_string(); - let _ = send( + let id = raisfast::utils::id::new_id().to_string(); + let (create_status, _) = send( &mut app, post_json_auth( "/api/v1/admin/workflows", @@ -713,8 +724,9 @@ async fn workflow_parallel_step_logs() { ), ) .await; + assert!(create_status.is_success(), "create: {create_status}"); - let (_, body) = send( + let (start_status, body) = send( &mut app, post_json_auth( &format!("/api/v1/admin/workflows/{id}/start"), @@ -723,7 +735,8 @@ async fn workflow_parallel_step_logs() { ), ) .await; - let instance_id = body["data"]["document_id"].as_str().unwrap().to_string(); + assert!(start_status.is_success(), "start: {start_status}"); + let instance_id = body["data"]["id"].as_str().unwrap().to_string(); // Execute parallel → spawns s2 + s3 send( diff --git a/tests/content_type_tests.rs b/tests/content_type_tests.rs index ac50441e..d2905b0e 100644 --- a/tests/content_type_tests.rs +++ b/tests/content_type_tests.rs @@ -203,7 +203,12 @@ async fn create_and_find_by_id() { assert_eq!(created["price"], 99); let found = repo - .find_by_id(&ct, created["document_id"].as_str().unwrap(), None, true) + .find_by_id( + &ct, + created["id"].as_str().unwrap().parse().unwrap(), + None, + true, + ) .await .unwrap() .unwrap(); @@ -340,12 +345,12 @@ async fn update_changes_fields() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); let updated = repo .update( &ct, - &id, + id, json!({"title": "Updated", "price": 99}), None, &SaveContext::default(), @@ -374,11 +379,11 @@ async fn delete_removes_record() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); repo.delete( &ct, - &id, + id, None, &test_protocol_registry(), &test_ct_registry(), @@ -386,7 +391,7 @@ async fn delete_removes_record() { .await .unwrap(); - let found = repo.find_by_id(&ct, &id, None, true).await.unwrap(); + let found = repo.find_by_id(&ct, id, None, true).await.unwrap(); assert!(found.is_none()); let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM ct_products") @@ -428,11 +433,11 @@ required = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); repo.delete( &ct, - &id, + id, None, &test_protocol_registry(), &test_ct_registry(), @@ -440,12 +445,11 @@ required = true .await .unwrap(); - let row: Option<(String,)> = - sqlx::query_as("SELECT deleted_at FROM ct_notes WHERE document_id = ?") - .bind(&id) - .fetch_optional(&repo.pool) - .await - .unwrap(); + let row: Option<(String,)> = sqlx::query_as("SELECT deleted_at FROM ct_notes WHERE id = ?") + .bind(id) + .fetch_optional(&repo.pool) + .await + .unwrap(); let deleted_at = row.unwrap().0; assert!(!deleted_at.is_empty()); @@ -501,8 +505,8 @@ async fn tenant_isolation() { .await .unwrap(); - let id_a = a["document_id"].as_str().unwrap(); - let id_b = b["document_id"].as_str().unwrap(); + let id_a: i64 = a["id"].as_str().unwrap().parse().unwrap(); + let id_b: i64 = b["id"].as_str().unwrap().parse().unwrap(); assert!( repo.find_by_id(&ct, id_a, Some("tenant_b"), true) @@ -564,7 +568,7 @@ async fn delete_respects_tenant() { ) .await .unwrap(); - let id_a = a["document_id"].as_str().unwrap(); + let id_a: i64 = a["id"].as_str().unwrap().parse().unwrap(); repo.delete( &ct, @@ -749,9 +753,7 @@ async fn create_auto_generates_id_and_timestamps() { .await .unwrap(); - assert!(result["id"].is_i64()); - assert!(result["document_id"].is_string()); - assert!(!result["document_id"].as_str().unwrap().is_empty()); + assert!(result["id"].is_string()); assert!(result.get("created_at").is_some()); assert!(result.get("updated_at").is_some()); } @@ -785,12 +787,12 @@ async fn update_with_no_fields_returns_error() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); let result = repo .update( &ct, - &id, + id, json!({"nonexistent_field": "v"}), None, &SaveContext::default(), @@ -916,11 +918,12 @@ async fn versioning_creates_revision_on_update() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); let _updated = repo .update( &ct, - id, + int_id, json!({"title": "V2 Title", "content": "V2 Content"}), None, &SaveContext::default(), @@ -928,17 +931,25 @@ async fn versioning_creates_revision_on_update() { .await .unwrap(); - let revisions = raisfast::models::content_revision::list_revisions(&pool, "article", id) - .await - .unwrap(); + let revisions = raisfast::models::content_revision::list_revisions( + &pool, + "article", + id.parse::().unwrap(), + ) + .await + .unwrap(); assert_eq!(revisions.len(), 1); assert_eq!(revisions[0].revision_number, 1); - let rev = - raisfast::models::content_revision::get_revision(&pool, "article", id, revisions[0].id) - .await - .unwrap() - .unwrap(); + let rev = raisfast::models::content_revision::get_revision( + &pool, + "article", + id.parse::().unwrap(), + *revisions[0].id, + ) + .await + .unwrap() + .unwrap(); let snapshot: serde_json::Value = serde_json::from_str(&rev.snapshot).unwrap(); assert_eq!(snapshot["title"], "V1 Title"); assert_eq!(snapshot["content"], "V1 Content"); @@ -961,10 +972,11 @@ async fn versioning_multiple_updates_create_multiple_revisions() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); repo.update( &ct, - id, + int_id, json!({"title": "Rev1"}), None, &SaveContext::default(), @@ -973,7 +985,7 @@ async fn versioning_multiple_updates_create_multiple_revisions() { .unwrap(); repo.update( &ct, - id, + int_id, json!({"title": "Rev2"}), None, &SaveContext::default(), @@ -982,7 +994,7 @@ async fn versioning_multiple_updates_create_multiple_revisions() { .unwrap(); repo.update( &ct, - id, + int_id, json!({"title": "Rev3"}), None, &SaveContext::default(), @@ -990,9 +1002,13 @@ async fn versioning_multiple_updates_create_multiple_revisions() { .await .unwrap(); - let revisions = raisfast::models::content_revision::list_revisions(&pool, "article", id) - .await - .unwrap(); + let revisions = raisfast::models::content_revision::list_revisions( + &pool, + "article", + id.parse::().unwrap(), + ) + .await + .unwrap(); assert_eq!(revisions.len(), 3); assert_eq!(revisions[0].revision_number, 3); assert_eq!(revisions[1].revision_number, 2); @@ -1016,11 +1032,12 @@ async fn versioning_delete_cleans_up_revisions() { ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); repo.update( &ct, - id, + int_id, json!({"title": "Updated"}), None, &SaveContext::default(), @@ -1028,14 +1045,18 @@ async fn versioning_delete_cleans_up_revisions() { .await .unwrap(); - let before = raisfast::models::content_revision::list_revisions(&pool, "article", id) - .await - .unwrap(); + let before = raisfast::models::content_revision::list_revisions( + &pool, + "article", + id.parse::().unwrap(), + ) + .await + .unwrap(); assert_eq!(before.len(), 1); repo.delete( &ct, - id, + int_id, None, &test_protocol_registry(), &test_ct_registry(), @@ -1043,9 +1064,13 @@ async fn versioning_delete_cleans_up_revisions() { .await .unwrap(); - let after = raisfast::models::content_revision::list_revisions(&pool, "article", id) - .await - .unwrap(); + let after = raisfast::models::content_revision::list_revisions( + &pool, + "article", + id.parse::().unwrap(), + ) + .await + .unwrap(); assert!(after.is_empty()); } @@ -1082,11 +1107,12 @@ required = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); repo.update( &ct, - id, + int_id, json!({"title": "Updated"}), None, &SaveContext::default(), @@ -1094,9 +1120,13 @@ required = true .await .unwrap(); - let revisions = raisfast::models::content_revision::list_revisions(&pool, "note", id) - .await - .unwrap(); + let revisions = raisfast::models::content_revision::list_revisions( + &pool, + "note", + id.parse::().unwrap(), + ) + .await + .unwrap(); assert!(revisions.is_empty()); } @@ -1164,7 +1194,7 @@ target_field = "title" ) .await .unwrap(); - let deleted_id = deleted["document_id"].as_str().unwrap(); + let deleted_id: i64 = deleted["id"].as_str().unwrap().parse().unwrap(); let now = now_str(); repo.soft_delete(&ct, deleted_id, &now, None, None) @@ -1230,10 +1260,13 @@ target_field = "title" ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); let now = now_str(); - repo.soft_delete(&ct, id, &now, None, None).await.unwrap(); + repo.soft_delete(&ct, int_id, &now, None, None) + .await + .unwrap(); let found = repo .find_by_slug(&ct, "gone", None, None, true) @@ -1274,12 +1307,15 @@ required = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); let now = now_str(); - repo.soft_delete(&ct, id, &now, None, None).await.unwrap(); + repo.soft_delete(&ct, int_id, &now, None, None) + .await + .unwrap(); - let found = repo.find_by_id(&ct, id, None, true).await.unwrap(); + let found = repo.find_by_id(&ct, int_id, None, true).await.unwrap(); assert!(found.is_some()); assert_eq!(found.unwrap()["title"].as_str().unwrap(), "Soft Deleted"); } @@ -1316,20 +1352,19 @@ required = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); let now = now_str(); repo.soft_delete(&ct, id, &now, Some(42), None) .await .unwrap(); - let row: (String, i64) = sqlx::query_as( - "SELECT deleted_at, deleted_by FROM ct_soft_deleted_by WHERE document_id = ?", - ) - .bind(id) - .fetch_one(&pool) - .await - .unwrap(); + let row: (String, i64) = + sqlx::query_as("SELECT deleted_at, deleted_by FROM ct_soft_deleted_by WHERE id = ?") + .bind(id) + .fetch_one(&pool) + .await + .unwrap(); assert!(!row.0.is_empty()); assert_eq!(row.1, 42); } @@ -1369,7 +1404,7 @@ required = true ) .await .unwrap(); - let id = created["document_id"].as_str().unwrap(); + let id: i64 = created["id"].as_str().unwrap().parse().unwrap(); let now = now_str(); repo.soft_delete(&ct, id, &now, None, None).await.unwrap(); diff --git a/tests/tauri_tests.rs b/tests/tauri_tests.rs index 69740065..0ee262b4 100644 --- a/tests/tauri_tests.rs +++ b/tests/tauri_tests.rs @@ -76,8 +76,8 @@ async fn create_test_user(pool: &sqlx::SqlitePool, label: &str) -> (i64, String) let user = auth::register(&aspect_engine, req, None, false, pool) .await .unwrap(); - let row: (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?") - .bind(&user.id) + let row: (i64,) = sqlx::query_as("SELECT id FROM users WHERE id = ?") + .bind(user.id.parse::().unwrap()) .fetch_one(pool) .await .unwrap(); @@ -253,8 +253,7 @@ async fn tauri_auth_get_me_service() { .unwrap(); let auth = raisfast::middleware::auth::AuthUser::from_parts( - Some(user.id.clone()), - None, + Some(user.id.parse().unwrap()), raisfast::models::user::UserRole::Author, None, ); @@ -270,7 +269,7 @@ async fn tauri_auth_get_me_service() { #[tokio::test] async fn tauri_post_create_and_list() { let pool = setup_pool().await; - let (author_int_id, author_doc_id) = create_test_user(&pool, "author-001").await; + let (author_int_id, _author_id) = create_test_user(&pool, "author-001").await; let svc = build_post_service(Arc::new(pool.clone())); let req = raisfast::dto::CreatePostRequest { @@ -284,7 +283,6 @@ async fn tauri_post_create_and_list() { }; let auth = raisfast::middleware::auth::AuthUser::from_parts( - Some(author_doc_id.clone()), Some(author_int_id), raisfast::models::user::UserRole::Author, None, @@ -292,7 +290,7 @@ async fn tauri_post_create_and_list() { let created = svc.create(&auth, req).await.unwrap(); assert_eq!(created.title, "Test Post"); - assert_eq!(created.created_by, author_int_id); + assert!(created.created_by.is_none()); let (items, total) = svc.list(&auth, 1, 20, None, None, None).await.unwrap(); @@ -303,7 +301,7 @@ async fn tauri_post_create_and_list() { #[tokio::test] async fn tauri_post_get_by_slug() { let pool = setup_pool().await; - let (author_int_id, author_doc_id) = create_test_user(&pool, "author-002").await; + let (author_int_id, _author_id) = create_test_user(&pool, "author-002").await; let svc = build_post_service(Arc::new(pool.clone())); let req = raisfast::dto::CreatePostRequest { @@ -317,7 +315,6 @@ async fn tauri_post_get_by_slug() { }; let auth = raisfast::middleware::auth::AuthUser::from_parts( - Some(author_doc_id.clone()), Some(author_int_id), raisfast::models::user::UserRole::Author, None, @@ -388,8 +385,13 @@ async fn tauri_cms_get_by_id() { let data = with_timestamps(serde_json::json!({"title": "Read book", "done": false})); let created = repo.create(&ct, data, None, &save_ctx).await.unwrap(); - let id = created["document_id"].as_str().unwrap(); - let found = repo.find_by_id(&ct, id, None, true).await.unwrap().unwrap(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); + let found = repo + .find_by_id(&ct, int_id, None, true) + .await + .unwrap() + .unwrap(); assert_eq!(found["title"], "Read book"); } @@ -404,14 +406,15 @@ async fn tauri_cms_update() { let data = with_timestamps(serde_json::json!({"title": "Original", "done": false})); let created = repo.create(&ct, data, None, &save_ctx).await.unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); let update_data = serde_json::json!({"title": "Updated", "done": true}); - repo.update(&ct, &id, update_data, None, &save_ctx) + repo.update(&ct, int_id, update_data, None, &save_ctx) .await .unwrap(); let found = repo - .find_by_id(&ct, &id, None, true) + .find_by_id(&ct, int_id, None, true) .await .unwrap() .unwrap(); @@ -429,10 +432,11 @@ async fn tauri_cms_delete() { let data = with_timestamps(serde_json::json!({"title": "To delete", "done": false})); let created = repo.create(&ct, data, None, &save_ctx).await.unwrap(); - let id = created["document_id"].as_str().unwrap().to_string(); + let id = created["id"].as_str().unwrap().to_string(); + let int_id: i64 = id.parse().unwrap(); repo.delete( &ct, - &id, + int_id, None, &test_protocol_registry(), &raisfast::content_type::ContentTypeRegistry::new(), @@ -440,7 +444,7 @@ async fn tauri_cms_delete() { .await .unwrap(); - let found = repo.find_by_id(&ct, &id, None, true).await.unwrap(); + let found = repo.find_by_id(&ct, int_id, None, true).await.unwrap(); assert!(found.is_none(), "deleted item should not exist"); }