diff --git a/Cargo.lock b/Cargo.lock index ddb72cf..97441a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,12 +22,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.8" @@ -80,24 +101,183 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -114,6 +294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -122,6 +303,40 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -135,17 +350,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.4.0" @@ -227,6 +498,108 @@ dependencies = [ "tower-service", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -234,7 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -255,6 +628,32 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -358,7 +757,9 @@ dependencies = [ "mcpaas-schema", "serde", "serde_json", + "sqlx", "thiserror", + "tokio", ] [[package]] @@ -407,7 +808,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -416,7 +817,16 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -425,6 +835,35 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -443,6 +882,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -461,6 +915,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -478,12 +941,66 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -563,6 +1080,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -572,6 +1100,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.12" @@ -591,9 +1125,136 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -611,6 +1272,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -640,18 +1312,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -665,6 +1348,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -755,6 +1449,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -767,24 +1467,87 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -794,6 +1557,159 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index dd961d8..eb807a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ axum = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "sqlite"] } thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tracing = "0.1" diff --git a/TASKS.md b/TASKS.md index fa10302..7143f3d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,19 +2,21 @@ ## Current -### `feat/mapping-engine` +### `feat/registry-storage` Status: completed DoD: -- JSONPath parsing works -- input and output mapping work -- draft generation from samples works +- migrations create tables from `database-schema` +- version snapshots persist and load correctly +- publish linkage works +- auth profiles and artifact metadata are stored +- registry integration tests run on a real SQLite database ## Next -- `feat/registry-storage` +- `feat/rest-vertical-slice` ## Backlog diff --git a/crates/mcpaas-registry/Cargo.toml b/crates/mcpaas-registry/Cargo.toml index 47eccbe..a65873c 100644 --- a/crates/mcpaas-registry/Cargo.toml +++ b/crates/mcpaas-registry/Cargo.toml @@ -11,5 +11,8 @@ mcpaas-mapping = { path = "../mcpaas-mapping" } mcpaas-schema = { path = "../mcpaas-schema" } serde.workspace = true serde_json.workspace = true +sqlx.workspace = true thiserror.workspace = true +[dev-dependencies] +tokio.workspace = true diff --git a/crates/mcpaas-registry/src/error.rs b/crates/mcpaas-registry/src/error.rs new file mode 100644 index 0000000..1837ca4 --- /dev/null +++ b/crates/mcpaas-registry/src/error.rs @@ -0,0 +1,36 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum RegistryError { + #[error(transparent)] + Storage(#[from] sqlx::Error), + #[error(transparent)] + Serialization(#[from] serde_json::Error), + #[error("operation {operation_id} already exists")] + OperationAlreadyExists { operation_id: String }, + #[error("operation {operation_id} was not found")] + OperationNotFound { operation_id: String }, + #[error("operation version {version} for {operation_id} was not found")] + OperationVersionNotFound { operation_id: String, version: u32 }, + #[error("operation {operation_id} must start with version 1, got {version}")] + InvalidInitialVersion { operation_id: String, version: u32 }, + #[error("operation {operation_id} expected next version {expected}, got {actual}")] + InvalidVersionSequence { + operation_id: String, + expected: u32, + actual: u32, + }, + #[error("operation {operation_id} changed immutable field {field}")] + ImmutableOperationFieldChanged { + operation_id: String, + field: &'static str, + }, + #[error("auth profile {auth_profile_id} was not found")] + AuthProfileNotFound { auth_profile_id: String }, + #[error("yaml import job {job_id} was not found")] + YamlImportJobNotFound { job_id: String }, + #[error("unsupported enum representation for field {field}")] + InvalidEnumRepresentation { field: &'static str }, + #[error("invalid numeric value for field {field}: {value}")] + InvalidNumericValue { field: &'static str, value: i64 }, +} diff --git a/crates/mcpaas-registry/src/lib.rs b/crates/mcpaas-registry/src/lib.rs index dee68c0..a912c2e 100644 --- a/crates/mcpaas-registry/src/lib.rs +++ b/crates/mcpaas-registry/src/lib.rs @@ -1,3 +1,14 @@ -pub fn crate_name() -> &'static str { - "mcpaas-registry" -} +mod error; +mod migrations; +mod model; +mod sqlite; + +pub use error::RegistryError; +pub use model::{ + CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata, + OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishRequest, + RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest, + SaveSampleMetadataRequest, YamlImportJob, YamlImportJobCompletion, YamlImportJobId, + YamlImportJobStatus, +}; +pub use sqlite::SqliteRegistry; diff --git a/crates/mcpaas-registry/src/migrations.rs b/crates/mcpaas-registry/src/migrations.rs new file mode 100644 index 0000000..19a07da --- /dev/null +++ b/crates/mcpaas-registry/src/migrations.rs @@ -0,0 +1,126 @@ +use sqlx::{SqlitePool, query}; + +pub async fn apply_sqlite(pool: &SqlitePool) -> Result<(), sqlx::Error> { + query( + "create table if not exists operations ( + id text primary key, + name text not null unique, + display_name text not null, + protocol text not null, + status text not null, + current_draft_version integer not null default 1, + latest_published_version integer null, + created_at text not null, + updated_at text not null, + published_at text null + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists operation_versions ( + operation_id text not null, + version integer not null, + status text not null, + target_json text not null, + input_schema_json text not null, + output_schema_json text not null, + input_mapping_json text not null, + output_mapping_json text not null, + execution_config_json text not null, + tool_description_json text not null, + samples_json text null, + generated_draft_json text null, + config_export_json text null, + change_note text null, + created_at text not null, + created_by text null, + primary key (operation_id, version), + foreign key (operation_id) references operations(id) on delete cascade + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists published_operations ( + operation_id text primary key, + version integer not null, + published_at text not null, + published_by text null, + foreign key (operation_id) references operations(id) on delete cascade, + foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists operation_samples ( + id text primary key, + operation_id text not null, + version integer not null, + sample_kind text not null, + storage_ref text not null, + content_type text not null, + file_name text null, + created_at text not null, + foreign key (operation_id) references operations(id) on delete cascade, + foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists descriptors ( + id text primary key, + operation_id text null, + version integer null, + descriptor_kind text not null, + storage_ref text not null, + source_name text null, + package_index_json text null, + created_at text not null, + foreign key (operation_id) references operations(id) on delete cascade, + foreign key (operation_id, version) references operation_versions(operation_id, version) on delete cascade + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists auth_profiles ( + id text primary key, + name text not null unique, + kind text not null, + config_json text not null, + created_at text not null, + updated_at text not null + )", + ) + .execute(pool) + .await?; + + query( + "create table if not exists yaml_import_jobs ( + id text primary key, + source_sample_id text null, + status text not null, + format_version text not null, + mode text not null, + result_operation_id text null, + result_version integer null, + error_text text null, + created_at text not null, + finished_at text null, + foreign key (source_sample_id) references operation_samples(id) on delete set null, + foreign key (result_operation_id) references operations(id) on delete set null + )", + ) + .execute(pool) + .await?; + + Ok(()) +} diff --git a/crates/mcpaas-registry/src/model.rs b/crates/mcpaas-registry/src/model.rs new file mode 100644 index 0000000..a827937 --- /dev/null +++ b/crates/mcpaas-registry/src/model.rs @@ -0,0 +1,176 @@ +use mcpaas_core::{ + AuthProfile, DescriptorId, ExportMode, Operation, OperationId, OperationStatus, Protocol, + SampleId, +}; +use mcpaas_mapping::MappingSet; +use mcpaas_schema::Schema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +macro_rules! define_registry_id { + ($name:ident) => { + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] + pub struct $name(String); + + impl $name { + pub fn new(value: impl Into) -> Self { + Self(value.into()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + } + + impl From for $name { + fn from(value: String) -> Self { + Self(value) + } + } + + impl From<&str> for $name { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } + } + }; +} + +define_registry_id!(YamlImportJobId); + +pub type RegistryOperation = Operation; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct OperationSummary { + pub id: OperationId, + pub name: String, + pub display_name: String, + pub protocol: Protocol, + pub status: OperationStatus, + pub current_draft_version: u32, + pub latest_published_version: Option, + pub created_at: String, + pub updated_at: String, + pub published_at: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct OperationVersionRecord { + pub operation_id: OperationId, + pub version: u32, + pub status: OperationStatus, + pub change_note: Option, + pub created_at: String, + pub created_by: Option, + pub snapshot: RegistryOperation, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SampleKind { + InputJson, + OutputJson, + YamlImportSource, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct OperationSampleMetadata { + pub id: SampleId, + pub operation_id: OperationId, + pub version: u32, + pub sample_kind: SampleKind, + pub storage_ref: String, + pub content_type: String, + pub file_name: Option, + pub created_at: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DescriptorKind { + ProtoUpload, + DescriptorSet, + ReflectionSnapshot, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct DescriptorMetadata { + pub id: DescriptorId, + pub operation_id: Option, + pub version: Option, + pub descriptor_kind: DescriptorKind, + pub storage_ref: String, + pub source_name: Option, + pub package_index: Option, + pub created_at: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum YamlImportJobStatus { + Pending, + Completed, + Failed, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct YamlImportJob { + pub id: YamlImportJobId, + pub source_sample_id: Option, + pub status: YamlImportJobStatus, + pub format_version: String, + pub mode: ExportMode, + pub result_operation_id: Option, + pub result_version: Option, + pub error_text: Option, + pub created_at: String, + pub finished_at: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct YamlImportJobCompletion { + pub status: YamlImportJobStatus, + pub result_operation_id: Option, + pub result_version: Option, + pub error_text: Option, + pub finished_at: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CreateVersionRequest<'a> { + pub snapshot: &'a RegistryOperation, + pub change_note: Option<&'a str>, + pub created_by: Option<&'a str>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PublishRequest<'a> { + pub operation_id: &'a OperationId, + pub version: u32, + pub published_at: &'a str, + pub published_by: Option<&'a str>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CreateYamlImportJobRequest<'a> { + pub id: &'a YamlImportJobId, + pub source_sample_id: Option<&'a SampleId>, + pub format_version: &'a str, + pub mode: ExportMode, + pub created_at: &'a str, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SaveAuthProfileRequest<'a> { + pub profile: &'a AuthProfile, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SaveSampleMetadataRequest<'a> { + pub sample: &'a OperationSampleMetadata, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SaveDescriptorMetadataRequest<'a> { + pub descriptor: &'a DescriptorMetadata, +} diff --git a/crates/mcpaas-registry/src/sqlite.rs b/crates/mcpaas-registry/src/sqlite.rs new file mode 100644 index 0000000..f784acc --- /dev/null +++ b/crates/mcpaas-registry/src/sqlite.rs @@ -0,0 +1,1351 @@ +use mcpaas_core::{AuthProfile, OperationId, OperationStatus}; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::Value; +use sqlx::{Row, SqlitePool, sqlite::SqlitePoolOptions}; + +use crate::{ + error::RegistryError, + migrations, + model::{ + CreateVersionRequest, CreateYamlImportJobRequest, DescriptorMetadata, + OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishRequest, + RegistryOperation, SaveAuthProfileRequest, SaveDescriptorMetadataRequest, + SaveSampleMetadataRequest, YamlImportJob, YamlImportJobCompletion, YamlImportJobId, + YamlImportJobStatus, + }, +}; + +#[derive(Clone, Debug)] +pub struct SqliteRegistry { + pool: SqlitePool, +} + +impl SqliteRegistry { + pub async fn connect(database_url: &str) -> Result { + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect(database_url) + .await?; + + sqlx::query("pragma foreign_keys = on") + .execute(&pool) + .await?; + + let registry = Self { pool }; + registry.migrate().await?; + Ok(registry) + } + + pub fn pool(&self) -> &SqlitePool { + &self.pool + } + + pub async fn migrate(&self) -> Result<(), RegistryError> { + migrations::apply_sqlite(&self.pool).await?; + Ok(()) + } + + pub async fn create_operation( + &self, + snapshot: &RegistryOperation, + created_by: Option<&str>, + ) -> Result<(), RegistryError> { + if snapshot.version != 1 { + return Err(RegistryError::InvalidInitialVersion { + operation_id: snapshot.id.as_str().to_owned(), + version: snapshot.version, + }); + } + + if self.get_operation_summary(&snapshot.id).await?.is_some() { + return Err(RegistryError::OperationAlreadyExists { + operation_id: snapshot.id.as_str().to_owned(), + }); + } + + let mut tx = self.pool.begin().await?; + + sqlx::query( + "insert into operations ( + id, + name, + display_name, + protocol, + status, + current_draft_version, + latest_published_version, + created_at, + updated_at, + published_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(snapshot.id.as_str()) + .bind(&snapshot.name) + .bind(&snapshot.display_name) + .bind(serialize_enum_text(&snapshot.protocol, "protocol")?) + .bind(serialize_enum_text(&snapshot.status, "status")?) + .bind(to_db_version(snapshot.version)) + .bind( + snapshot + .published_at + .as_ref() + .map(|_| to_db_version(snapshot.version)), + ) + .bind(&snapshot.created_at) + .bind(&snapshot.updated_at) + .bind(snapshot.published_at.as_deref()) + .execute(&mut *tx) + .await?; + + insert_version_row(&mut tx, snapshot, None, created_by).await?; + + tx.commit().await?; + Ok(()) + } + + pub async fn create_version( + &self, + request: CreateVersionRequest<'_>, + ) -> Result<(), RegistryError> { + let Some(summary) = self.get_operation_summary(&request.snapshot.id).await? else { + return Err(RegistryError::OperationNotFound { + operation_id: request.snapshot.id.as_str().to_owned(), + }); + }; + + assert_immutable_fields(&summary, request.snapshot)?; + + let expected = summary.current_draft_version + 1; + if request.snapshot.version != expected { + return Err(RegistryError::InvalidVersionSequence { + operation_id: request.snapshot.id.as_str().to_owned(), + expected, + actual: request.snapshot.version, + }); + } + + let mut tx = self.pool.begin().await?; + + insert_version_row( + &mut tx, + request.snapshot, + request.change_note, + request.created_by, + ) + .await?; + + sqlx::query( + "update operations + set status = ?, + current_draft_version = ?, + updated_at = ? + where id = ?", + ) + .bind(serialize_enum_text(&request.snapshot.status, "status")?) + .bind(to_db_version(request.snapshot.version)) + .bind(&request.snapshot.updated_at) + .bind(request.snapshot.id.as_str()) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) + } + + pub async fn list_operations(&self) -> Result, RegistryError> { + let rows = sqlx::query( + "select + id, + name, + display_name, + protocol, + status, + current_draft_version, + latest_published_version, + created_at, + updated_at, + published_at + from operations + order by name asc", + ) + .fetch_all(&self.pool) + .await?; + + rows.iter().map(map_operation_summary).collect() + } + + pub async fn get_operation_summary( + &self, + operation_id: &OperationId, + ) -> Result, RegistryError> { + let row = sqlx::query( + "select + id, + name, + display_name, + protocol, + status, + current_draft_version, + latest_published_version, + created_at, + updated_at, + published_at + from operations + where id = ?", + ) + .bind(operation_id.as_str()) + .fetch_optional(&self.pool) + .await?; + + row.as_ref().map(map_operation_summary).transpose() + } + + pub async fn get_operation_version( + &self, + operation_id: &OperationId, + version: u32, + ) -> Result, RegistryError> { + let row = sqlx::query( + "select + o.id, + o.name, + o.display_name, + o.protocol, + o.created_at as operation_created_at, + o.updated_at as operation_updated_at, + o.published_at as operation_published_at, + ov.version, + ov.status, + ov.target_json, + ov.input_schema_json, + ov.output_schema_json, + ov.input_mapping_json, + ov.output_mapping_json, + ov.execution_config_json, + ov.tool_description_json, + ov.samples_json, + ov.generated_draft_json, + ov.config_export_json, + ov.change_note, + ov.created_at, + ov.created_by + from operation_versions ov + join operations o on o.id = ov.operation_id + where ov.operation_id = ? and ov.version = ?", + ) + .bind(operation_id.as_str()) + .bind(to_db_version(version)) + .fetch_optional(&self.pool) + .await?; + + row.as_ref().map(map_operation_version_record).transpose() + } + + pub async fn list_operation_versions( + &self, + operation_id: &OperationId, + ) -> Result, RegistryError> { + let rows = sqlx::query( + "select + o.id, + o.name, + o.display_name, + o.protocol, + o.created_at as operation_created_at, + o.updated_at as operation_updated_at, + o.published_at as operation_published_at, + ov.version, + ov.status, + ov.target_json, + ov.input_schema_json, + ov.output_schema_json, + ov.input_mapping_json, + ov.output_mapping_json, + ov.execution_config_json, + ov.tool_description_json, + ov.samples_json, + ov.generated_draft_json, + ov.config_export_json, + ov.change_note, + ov.created_at, + ov.created_by + from operation_versions ov + join operations o on o.id = ov.operation_id + where ov.operation_id = ? + order by ov.version asc", + ) + .bind(operation_id.as_str()) + .fetch_all(&self.pool) + .await?; + + rows.iter().map(map_operation_version_record).collect() + } + + pub async fn publish_operation( + &self, + request: PublishRequest<'_>, + ) -> Result<(), RegistryError> { + if self + .get_operation_version(request.operation_id, request.version) + .await? + .is_none() + { + return Err(RegistryError::OperationVersionNotFound { + operation_id: request.operation_id.as_str().to_owned(), + version: request.version, + }); + } + + let mut tx = self.pool.begin().await?; + + sqlx::query( + "insert into published_operations ( + operation_id, + version, + published_at, + published_by + ) values (?, ?, ?, ?) + on conflict(operation_id) do update set + version = excluded.version, + published_at = excluded.published_at, + published_by = excluded.published_by", + ) + .bind(request.operation_id.as_str()) + .bind(to_db_version(request.version)) + .bind(request.published_at) + .bind(request.published_by) + .execute(&mut *tx) + .await?; + + sqlx::query( + "update operation_versions + set status = ? + where operation_id = ? and version = ?", + ) + .bind(serialize_enum_text(&OperationStatus::Published, "status")?) + .bind(request.operation_id.as_str()) + .bind(to_db_version(request.version)) + .execute(&mut *tx) + .await?; + + sqlx::query( + "update operations + set status = ?, + latest_published_version = ?, + published_at = ?, + updated_at = ? + where id = ?", + ) + .bind(serialize_enum_text(&OperationStatus::Published, "status")?) + .bind(to_db_version(request.version)) + .bind(request.published_at) + .bind(request.published_at) + .bind(request.operation_id.as_str()) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) + } + + pub async fn get_published_operation( + &self, + operation_id: &OperationId, + ) -> Result, RegistryError> { + let row = sqlx::query( + "select + o.id, + o.name, + o.display_name, + o.protocol, + o.created_at as operation_created_at, + o.updated_at as operation_updated_at, + o.published_at as operation_published_at, + ov.version, + ov.status, + ov.target_json, + ov.input_schema_json, + ov.output_schema_json, + ov.input_mapping_json, + ov.output_mapping_json, + ov.execution_config_json, + ov.tool_description_json, + ov.samples_json, + ov.generated_draft_json, + ov.config_export_json, + ov.change_note, + ov.created_at, + ov.created_by + from published_operations po + join operation_versions ov + on ov.operation_id = po.operation_id and ov.version = po.version + join operations o on o.id = po.operation_id + where po.operation_id = ?", + ) + .bind(operation_id.as_str()) + .fetch_optional(&self.pool) + .await?; + + row.as_ref() + .map(|value| map_operation_version_record(value).map(|record| record.snapshot)) + .transpose() + } + + pub async fn list_published_operations(&self) -> Result, RegistryError> { + let rows = sqlx::query( + "select + o.id, + o.name, + o.display_name, + o.protocol, + o.created_at as operation_created_at, + o.updated_at as operation_updated_at, + o.published_at as operation_published_at, + ov.version, + ov.status, + ov.target_json, + ov.input_schema_json, + ov.output_schema_json, + ov.input_mapping_json, + ov.output_mapping_json, + ov.execution_config_json, + ov.tool_description_json, + ov.samples_json, + ov.generated_draft_json, + ov.config_export_json, + ov.change_note, + ov.created_at, + ov.created_by + from published_operations po + join operation_versions ov + on ov.operation_id = po.operation_id and ov.version = po.version + join operations o on o.id = po.operation_id + order by o.name asc", + ) + .fetch_all(&self.pool) + .await?; + + rows.iter() + .map(|row| map_operation_version_record(row).map(|record| record.snapshot)) + .collect() + } + + pub async fn save_auth_profile( + &self, + request: SaveAuthProfileRequest<'_>, + ) -> Result<(), RegistryError> { + sqlx::query( + "insert into auth_profiles ( + id, + name, + kind, + config_json, + created_at, + updated_at + ) values (?, ?, ?, ?, ?, ?) + on conflict(id) do update set + name = excluded.name, + kind = excluded.kind, + config_json = excluded.config_json, + updated_at = excluded.updated_at", + ) + .bind(request.profile.id.as_str()) + .bind(&request.profile.name) + .bind(serialize_enum_text(&request.profile.kind, "kind")?) + .bind(serialize_json(&request.profile.config)?) + .bind(&request.profile.created_at) + .bind(&request.profile.updated_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn get_auth_profile( + &self, + auth_profile_id: &mcpaas_core::AuthProfileId, + ) -> Result, RegistryError> { + let row = sqlx::query( + "select id, name, kind, config_json, created_at, updated_at + from auth_profiles + where id = ?", + ) + .bind(auth_profile_id.as_str()) + .fetch_optional(&self.pool) + .await?; + + row.as_ref().map(map_auth_profile).transpose() + } + + pub async fn list_auth_profiles(&self) -> Result, RegistryError> { + let rows = sqlx::query( + "select id, name, kind, config_json, created_at, updated_at + from auth_profiles + order by name asc", + ) + .fetch_all(&self.pool) + .await?; + + rows.iter().map(map_auth_profile).collect() + } + + pub async fn save_sample_metadata( + &self, + request: SaveSampleMetadataRequest<'_>, + ) -> Result<(), RegistryError> { + sqlx::query( + "insert into operation_samples ( + id, + operation_id, + version, + sample_kind, + storage_ref, + content_type, + file_name, + created_at + ) values (?, ?, ?, ?, ?, ?, ?, ?) + on conflict(id) do update set + operation_id = excluded.operation_id, + version = excluded.version, + sample_kind = excluded.sample_kind, + storage_ref = excluded.storage_ref, + content_type = excluded.content_type, + file_name = excluded.file_name, + created_at = excluded.created_at", + ) + .bind(request.sample.id.as_str()) + .bind(request.sample.operation_id.as_str()) + .bind(to_db_version(request.sample.version)) + .bind(serialize_enum_text( + &request.sample.sample_kind, + "sample_kind", + )?) + .bind(&request.sample.storage_ref) + .bind(&request.sample.content_type) + .bind(request.sample.file_name.as_deref()) + .bind(&request.sample.created_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn list_sample_metadata( + &self, + operation_id: &OperationId, + version: u32, + ) -> Result, RegistryError> { + let rows = sqlx::query( + "select + id, + operation_id, + version, + sample_kind, + storage_ref, + content_type, + file_name, + created_at + from operation_samples + where operation_id = ? and version = ? + order by created_at asc", + ) + .bind(operation_id.as_str()) + .bind(to_db_version(version)) + .fetch_all(&self.pool) + .await?; + + rows.iter().map(map_sample_metadata).collect() + } + + pub async fn save_descriptor_metadata( + &self, + request: SaveDescriptorMetadataRequest<'_>, + ) -> Result<(), RegistryError> { + sqlx::query( + "insert into descriptors ( + id, + operation_id, + version, + descriptor_kind, + storage_ref, + source_name, + package_index_json, + created_at + ) values (?, ?, ?, ?, ?, ?, ?, ?) + on conflict(id) do update set + operation_id = excluded.operation_id, + version = excluded.version, + descriptor_kind = excluded.descriptor_kind, + storage_ref = excluded.storage_ref, + source_name = excluded.source_name, + package_index_json = excluded.package_index_json, + created_at = excluded.created_at", + ) + .bind(request.descriptor.id.as_str()) + .bind( + request + .descriptor + .operation_id + .as_ref() + .map(|value| value.as_str()), + ) + .bind(request.descriptor.version.map(to_db_version)) + .bind(serialize_enum_text( + &request.descriptor.descriptor_kind, + "descriptor_kind", + )?) + .bind(&request.descriptor.storage_ref) + .bind(request.descriptor.source_name.as_deref()) + .bind(serialize_option_json(&request.descriptor.package_index)?) + .bind(&request.descriptor.created_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn list_descriptor_metadata( + &self, + operation_id: &OperationId, + version: u32, + ) -> Result, RegistryError> { + let rows = sqlx::query( + "select + id, + operation_id, + version, + descriptor_kind, + storage_ref, + source_name, + package_index_json, + created_at + from descriptors + where operation_id = ? and version = ? + order by created_at asc", + ) + .bind(operation_id.as_str()) + .bind(to_db_version(version)) + .fetch_all(&self.pool) + .await?; + + rows.iter().map(map_descriptor_metadata).collect() + } + + pub async fn create_yaml_import_job( + &self, + request: CreateYamlImportJobRequest<'_>, + ) -> Result<(), RegistryError> { + sqlx::query( + "insert into yaml_import_jobs ( + id, + source_sample_id, + status, + format_version, + mode, + result_operation_id, + result_version, + error_text, + created_at, + finished_at + ) values (?, ?, ?, ?, ?, null, null, null, ?, null)", + ) + .bind(request.id.as_str()) + .bind(request.source_sample_id.map(|value| value.as_str())) + .bind(serialize_enum_text( + &YamlImportJobStatus::Pending, + "status", + )?) + .bind(request.format_version) + .bind(serialize_enum_text(&request.mode, "mode")?) + .bind(request.created_at) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn finish_yaml_import_job( + &self, + job_id: &YamlImportJobId, + completion: &YamlImportJobCompletion, + ) -> Result<(), RegistryError> { + let result = sqlx::query( + "update yaml_import_jobs + set status = ?, + result_operation_id = ?, + result_version = ?, + error_text = ?, + finished_at = ? + where id = ?", + ) + .bind(serialize_enum_text(&completion.status, "status")?) + .bind( + completion + .result_operation_id + .as_ref() + .map(|value| value.as_str()), + ) + .bind(completion.result_version.map(to_db_version)) + .bind(completion.error_text.as_deref()) + .bind(&completion.finished_at) + .bind(job_id.as_str()) + .execute(&self.pool) + .await?; + + if result.rows_affected() == 0 { + return Err(RegistryError::YamlImportJobNotFound { + job_id: job_id.as_str().to_owned(), + }); + } + + Ok(()) + } + + pub async fn get_yaml_import_job( + &self, + job_id: &YamlImportJobId, + ) -> Result, RegistryError> { + let row = sqlx::query( + "select + id, + source_sample_id, + status, + format_version, + mode, + result_operation_id, + result_version, + error_text, + created_at, + finished_at + from yaml_import_jobs + where id = ?", + ) + .bind(job_id.as_str()) + .fetch_optional(&self.pool) + .await?; + + row.as_ref().map(map_yaml_import_job).transpose() + } +} + +async fn insert_version_row( + tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, + snapshot: &RegistryOperation, + change_note: Option<&str>, + created_by: Option<&str>, +) -> Result<(), RegistryError> { + sqlx::query( + "insert into operation_versions ( + operation_id, + version, + status, + target_json, + input_schema_json, + output_schema_json, + input_mapping_json, + output_mapping_json, + execution_config_json, + tool_description_json, + samples_json, + generated_draft_json, + config_export_json, + change_note, + created_at, + created_by + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(snapshot.id.as_str()) + .bind(to_db_version(snapshot.version)) + .bind(serialize_enum_text(&snapshot.status, "status")?) + .bind(serialize_json(&snapshot.target)?) + .bind(serialize_json(&snapshot.input_schema)?) + .bind(serialize_json(&snapshot.output_schema)?) + .bind(serialize_json(&snapshot.input_mapping)?) + .bind(serialize_json(&snapshot.output_mapping)?) + .bind(serialize_json(&snapshot.execution_config)?) + .bind(serialize_json(&snapshot.tool_description)?) + .bind(serialize_option_json(&snapshot.samples)?) + .bind(serialize_option_json(&snapshot.generated_draft)?) + .bind(serialize_option_json(&snapshot.config_export)?) + .bind(change_note) + .bind(&snapshot.updated_at) + .bind(created_by) + .execute(&mut **tx) + .await?; + + Ok(()) +} + +fn assert_immutable_fields( + summary: &OperationSummary, + snapshot: &RegistryOperation, +) -> Result<(), RegistryError> { + if summary.name != snapshot.name { + return Err(RegistryError::ImmutableOperationFieldChanged { + operation_id: snapshot.id.as_str().to_owned(), + field: "name", + }); + } + + if summary.display_name != snapshot.display_name { + return Err(RegistryError::ImmutableOperationFieldChanged { + operation_id: snapshot.id.as_str().to_owned(), + field: "display_name", + }); + } + + if summary.protocol != snapshot.protocol { + return Err(RegistryError::ImmutableOperationFieldChanged { + operation_id: snapshot.id.as_str().to_owned(), + field: "protocol", + }); + } + + Ok(()) +} + +fn map_operation_summary(row: &sqlx::sqlite::SqliteRow) -> Result { + Ok(OperationSummary { + id: OperationId::new(row.try_get::("id")?), + name: row.try_get("name")?, + display_name: row.try_get("display_name")?, + protocol: deserialize_enum_text(&row.try_get::("protocol")?, "protocol")?, + status: deserialize_enum_text(&row.try_get::("status")?, "status")?, + current_draft_version: from_db_version( + row.try_get("current_draft_version")?, + "current_draft_version", + )?, + latest_published_version: row + .try_get::, _>("latest_published_version")? + .map(|value| from_db_version(value, "latest_published_version")) + .transpose()?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + published_at: row.try_get("published_at")?, + }) +} + +fn map_operation_version_record( + row: &sqlx::sqlite::SqliteRow, +) -> Result { + let operation_id = OperationId::new(row.try_get::("id")?); + let version = from_db_version(row.try_get("version")?, "version")?; + let status = deserialize_enum_text(&row.try_get::("status")?, "status")?; + + Ok(OperationVersionRecord { + operation_id: operation_id.clone(), + version, + status, + change_note: row.try_get("change_note")?, + created_at: row.try_get("created_at")?, + created_by: row.try_get("created_by")?, + snapshot: RegistryOperation { + id: operation_id, + name: row.try_get("name")?, + display_name: row.try_get("display_name")?, + protocol: deserialize_enum_text(&row.try_get::("protocol")?, "protocol")?, + status, + version, + target: deserialize_json(&row.try_get::("target_json")?)?, + input_schema: deserialize_json(&row.try_get::("input_schema_json")?)?, + output_schema: deserialize_json(&row.try_get::("output_schema_json")?)?, + input_mapping: deserialize_json(&row.try_get::("input_mapping_json")?)?, + output_mapping: deserialize_json(&row.try_get::("output_mapping_json")?)?, + execution_config: deserialize_json( + &row.try_get::("execution_config_json")?, + )?, + tool_description: deserialize_json( + &row.try_get::("tool_description_json")?, + )?, + samples: row + .try_get::, _>("samples_json")? + .as_deref() + .map(deserialize_json) + .transpose()?, + generated_draft: row + .try_get::, _>("generated_draft_json")? + .as_deref() + .map(deserialize_json) + .transpose()?, + config_export: row + .try_get::, _>("config_export_json")? + .as_deref() + .map(deserialize_json) + .transpose()?, + created_at: row.try_get("operation_created_at")?, + updated_at: row.try_get("operation_updated_at")?, + published_at: row.try_get("operation_published_at")?, + }, + }) +} + +fn map_auth_profile(row: &sqlx::sqlite::SqliteRow) -> Result { + Ok(AuthProfile { + id: mcpaas_core::AuthProfileId::new(row.try_get::("id")?), + name: row.try_get("name")?, + kind: deserialize_enum_text(&row.try_get::("kind")?, "kind")?, + config: deserialize_json(&row.try_get::("config_json")?)?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }) +} + +fn map_sample_metadata( + row: &sqlx::sqlite::SqliteRow, +) -> Result { + Ok(OperationSampleMetadata { + id: mcpaas_core::SampleId::new(row.try_get::("id")?), + operation_id: OperationId::new(row.try_get::("operation_id")?), + version: from_db_version(row.try_get("version")?, "version")?, + sample_kind: deserialize_enum_text( + &row.try_get::("sample_kind")?, + "sample_kind", + )?, + storage_ref: row.try_get("storage_ref")?, + content_type: row.try_get("content_type")?, + file_name: row.try_get("file_name")?, + created_at: row.try_get("created_at")?, + }) +} + +fn map_descriptor_metadata( + row: &sqlx::sqlite::SqliteRow, +) -> Result { + Ok(DescriptorMetadata { + id: mcpaas_core::DescriptorId::new(row.try_get::("id")?), + operation_id: row + .try_get::, _>("operation_id")? + .map(OperationId::new), + version: row + .try_get::, _>("version")? + .map(|value| from_db_version(value, "version")) + .transpose()?, + descriptor_kind: deserialize_enum_text( + &row.try_get::("descriptor_kind")?, + "descriptor_kind", + )?, + storage_ref: row.try_get("storage_ref")?, + source_name: row.try_get("source_name")?, + package_index: row + .try_get::, _>("package_index_json")? + .as_deref() + .map(deserialize_json) + .transpose()?, + created_at: row.try_get("created_at")?, + }) +} + +fn map_yaml_import_job(row: &sqlx::sqlite::SqliteRow) -> Result { + Ok(YamlImportJob { + id: YamlImportJobId::new(row.try_get::("id")?), + source_sample_id: row + .try_get::, _>("source_sample_id")? + .map(mcpaas_core::SampleId::new), + status: deserialize_enum_text(&row.try_get::("status")?, "status")?, + format_version: row.try_get("format_version")?, + mode: deserialize_enum_text(&row.try_get::("mode")?, "mode")?, + result_operation_id: row + .try_get::, _>("result_operation_id")? + .map(OperationId::new), + result_version: row + .try_get::, _>("result_version")? + .map(|value| from_db_version(value, "result_version")) + .transpose()?, + error_text: row.try_get("error_text")?, + created_at: row.try_get("created_at")?, + finished_at: row.try_get("finished_at")?, + }) +} + +fn serialize_json(value: &T) -> Result { + Ok(serde_json::to_string(value)?) +} + +fn serialize_option_json(value: &Option) -> Result, RegistryError> { + value.as_ref().map(serialize_json).transpose() +} + +fn deserialize_json(value: &str) -> Result { + Ok(serde_json::from_str(value)?) +} + +fn serialize_enum_text( + value: &T, + field: &'static str, +) -> Result { + serde_json::to_value(value)? + .as_str() + .map(ToOwned::to_owned) + .ok_or(RegistryError::InvalidEnumRepresentation { field }) +} + +fn deserialize_enum_text( + value: &str, + field: &'static str, +) -> Result { + serde_json::from_value(Value::String(value.to_owned())) + .map_err(|_| RegistryError::InvalidEnumRepresentation { field }) +} + +fn to_db_version(value: u32) -> i64 { + i64::from(value) +} + +fn from_db_version(value: i64, field: &'static str) -> Result { + u32::try_from(value).map_err(|_| RegistryError::InvalidNumericValue { field, value }) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use mcpaas_core::{ + ApiKeyHeaderAuthConfig, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig, + ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, OperationId, OperationStatus, + Protocol, RestTarget, RetryPolicy, Samples, SecretRef, Target, ToolDescription, + ToolExample, + }; + use mcpaas_mapping::{MappingRule, MappingSet}; + use mcpaas_schema::{Schema, SchemaKind}; + use serde_json::json; + + use crate::{ + SqliteRegistry, + error::RegistryError, + model::{ + CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata, + OperationSampleMetadata, PublishRequest, RegistryOperation, SampleKind, + SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, + YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus, + }, + }; + + #[tokio::test] + async fn stores_versions_and_published_operations() { + let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft); + + registry + .create_operation(&operation_v1, Some("alice")) + .await + .unwrap(); + + let operation_v2 = test_operation("op_rest_01", 2, OperationStatus::Draft); + + registry + .create_version(CreateVersionRequest { + snapshot: &operation_v2, + change_note: Some("add output mapping"), + created_by: Some("alice"), + }) + .await + .unwrap(); + + registry + .publish_operation(PublishRequest { + operation_id: &operation_v2.id, + version: operation_v2.version, + published_at: "2026-03-25T12:10:00Z", + published_by: Some("alice"), + }) + .await + .unwrap(); + + let summary = registry + .get_operation_summary(&operation_v2.id) + .await + .unwrap() + .unwrap(); + let versions = registry + .list_operation_versions(&operation_v2.id) + .await + .unwrap(); + let published = registry + .get_published_operation(&operation_v2.id) + .await + .unwrap() + .unwrap(); + + assert_eq!(summary.current_draft_version, 2); + assert_eq!(summary.latest_published_version, Some(2)); + assert_eq!(summary.status, OperationStatus::Published); + assert_eq!(versions.len(), 2); + assert_eq!( + versions[1].change_note.as_deref(), + Some("add output mapping") + ); + assert_eq!(published.version, 2); + assert!(published.is_published()); + } + + #[tokio::test] + async fn rejects_out_of_order_versions() { + let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let operation = test_operation("op_rest_02", 1, OperationStatus::Draft); + + registry.create_operation(&operation, None).await.unwrap(); + + let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft); + let error = registry + .create_version(CreateVersionRequest { + snapshot: &invalid, + change_note: None, + created_by: None, + }) + .await + .unwrap_err(); + + assert!(matches!( + error, + RegistryError::InvalidVersionSequence { + expected: 2, + actual: 3, + .. + } + )); + } + + #[tokio::test] + async fn stores_auth_profiles_and_artifact_metadata() { + let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let operation = test_operation("op_rest_03", 1, OperationStatus::Draft); + + registry.create_operation(&operation, None).await.unwrap(); + + let auth_profile = AuthProfile { + id: "auth_rmcp".into(), + name: "RMCP API key".to_owned(), + kind: AuthKind::ApiKeyHeader, + config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig { + header_name: "X-Api-Key".to_owned(), + secret_ref: SecretRef::new("vault://rmcp/api-key"), + }), + created_at: "2026-03-25T12:00:00Z".to_owned(), + updated_at: "2026-03-25T12:00:00Z".to_owned(), + }; + + registry + .save_auth_profile(SaveAuthProfileRequest { + profile: &auth_profile, + }) + .await + .unwrap(); + + let input_sample = OperationSampleMetadata { + id: "sample_input".into(), + operation_id: operation.id.clone(), + version: 1, + sample_kind: SampleKind::InputJson, + storage_ref: "file:///tmp/input.json".to_owned(), + content_type: "application/json".to_owned(), + file_name: Some("input.json".to_owned()), + created_at: "2026-03-25T12:01:00Z".to_owned(), + }; + let descriptor = DescriptorMetadata { + id: "descriptor_01".into(), + operation_id: Some(operation.id.clone()), + version: Some(1), + descriptor_kind: DescriptorKind::DescriptorSet, + storage_ref: "file:///tmp/schema.desc".to_owned(), + source_name: Some("schema.desc".to_owned()), + package_index: Some(json!({ "crm.v1": ["LeadService"] })), + created_at: "2026-03-25T12:02:00Z".to_owned(), + }; + + registry + .save_sample_metadata(SaveSampleMetadataRequest { + sample: &input_sample, + }) + .await + .unwrap(); + registry + .save_descriptor_metadata(SaveDescriptorMetadataRequest { + descriptor: &descriptor, + }) + .await + .unwrap(); + + let auth_profiles = registry.list_auth_profiles().await.unwrap(); + let samples = registry + .list_sample_metadata(&operation.id, 1) + .await + .unwrap(); + let descriptors = registry + .list_descriptor_metadata(&operation.id, 1) + .await + .unwrap(); + + assert_eq!(auth_profiles, vec![auth_profile]); + assert_eq!(samples, vec![input_sample]); + assert_eq!(descriptors, vec![descriptor]); + } + + #[tokio::test] + async fn stores_and_finishes_yaml_import_jobs() { + let registry = SqliteRegistry::connect("sqlite::memory:").await.unwrap(); + let job_id = YamlImportJobId::new("job_yaml_01"); + let operation = test_operation("op_rest_04", 1, OperationStatus::Draft); + + registry.create_operation(&operation, None).await.unwrap(); + + registry + .create_yaml_import_job(CreateYamlImportJobRequest { + id: &job_id, + source_sample_id: None, + format_version: "v1", + mode: ExportMode::Portable, + created_at: "2026-03-25T12:00:00Z", + }) + .await + .unwrap(); + + registry + .finish_yaml_import_job( + &job_id, + &YamlImportJobCompletion { + status: YamlImportJobStatus::Completed, + result_operation_id: Some(operation.id.clone()), + result_version: Some(2), + error_text: None, + finished_at: "2026-03-25T12:05:00Z".to_owned(), + }, + ) + .await + .unwrap(); + + let job = registry + .get_yaml_import_job(&job_id) + .await + .unwrap() + .unwrap(); + + assert_eq!(job.status, YamlImportJobStatus::Completed); + assert_eq!(job.result_version, Some(2)); + assert_eq!(job.mode, ExportMode::Portable); + } + + fn test_operation(id: &str, version: u32, status: OperationStatus) -> RegistryOperation { + RegistryOperation { + id: OperationId::new(id), + name: format!("{id}_tool"), + display_name: format!("Display {id}"), + protocol: Protocol::Rest, + status, + version, + target: Target::Rest(RestTarget { + base_url: "https://api.example.com".to_owned(), + method: HttpMethod::Post, + path_template: "/v1/leads".to_owned(), + static_headers: BTreeMap::from([("X-Static".to_owned(), "true".to_owned())]), + }), + input_schema: Schema { + kind: SchemaKind::Object, + description: Some("input".to_owned()), + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::from([( + "email".to_owned(), + Schema { + kind: SchemaKind::String, + description: None, + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::new(), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + )]), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + output_schema: Schema { + kind: SchemaKind::Object, + description: Some("output".to_owned()), + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::from([( + "id".to_owned(), + Schema { + kind: SchemaKind::String, + description: None, + required: true, + nullable: false, + default_value: None, + fields: BTreeMap::new(), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + )]), + items: None, + enum_values: Vec::new(), + variants: Vec::new(), + }, + input_mapping: MappingSet { + rules: vec![MappingRule { + source: "$.mcp.email".to_owned(), + target: "$.request.body.email".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }], + }, + output_mapping: MappingSet { + rules: vec![MappingRule { + source: "$.response.body.id".to_owned(), + target: "$.output.id".to_owned(), + required: true, + default_value: None, + transform: None, + condition: None, + notes: None, + }], + }, + execution_config: ExecutionConfig { + timeout_ms: 10_000, + retry_policy: Some(RetryPolicy { max_attempts: 3 }), + auth_profile_ref: Some("auth_rmcp".into()), + headers: BTreeMap::from([("X-Request-Id".to_owned(), "static".to_owned())]), + protocol_options: None, + }, + tool_description: ToolDescription { + title: "Create lead".to_owned(), + description: "Creates CRM lead".to_owned(), + tags: vec!["crm".to_owned()], + examples: vec![ToolExample { + input: json!({ "email": "a@example.com" }), + }], + }, + samples: Some(Samples { + input_json_sample_ref: Some("sample_input".into()), + output_json_sample_ref: Some("sample_output".into()), + proto_file_ref: None, + descriptor_ref: None, + }), + generated_draft: Some(GeneratedDraft { + status: GeneratedDraftStatus::Available, + source_types: vec!["input_json".to_owned(), "output_json".to_owned()], + generated_at: Some("2026-03-25T11:59:00Z".to_owned()), + input_schema_generated: true, + output_schema_generated: true, + input_mapping_generated: true, + output_mapping_generated: true, + warnings: Vec::new(), + }), + config_export: Some(ConfigExport { + format_version: "v1".to_owned(), + export_mode: ExportMode::Portable, + }), + created_at: "2026-03-25T11:58:00Z".to_owned(), + updated_at: format!("2026-03-25T12:{:02}:00Z", version), + published_at: None, + } + } +} diff --git a/docs/database-schema.md b/docs/database-schema.md index c983e36..717f0c0 100644 --- a/docs/database-schema.md +++ b/docs/database-schema.md @@ -12,6 +12,8 @@ Конфигурация operation не должна храниться только в одной "живой" записи. Каждое существенное изменение должно приводить к появлению новой версии конфигурации. +Для MVP в registry version snapshot хранит protocol-specific конфигурацию, схемы, mapping и execution settings. Поля identity и listing view (`name`, `display_name`, `protocol`) считаются стабильными и хранятся в `operations`. + ### 2.2. Published и draft разделяются логически - `draft` может меняться; @@ -28,6 +30,14 @@ Для MVP рекомендуется отдельная таблица `auth_profiles`, где metadata и secret refs отделены от operation versions. +### 2.5. SQLite-адаптация допустима + +Канонической схемой остается PostgreSQL-совместимая модель, но MVP-реализация registry может использовать SQLite. В таком случае: + +- `jsonb` хранится как `text` с JSON-сериализацией; +- `timestamptz` хранится как `text`; +- внешние ключи и version snapshot semantics сохраняются без изменения. + ## 3. Основные таблицы Минимальный набор таблиц: