diff --git a/.env.example b/.env.example index 8e81a063..200b1aff 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,15 @@ HOSTNAME="---" ACTIX_KEEP_ALIVE=120 MAX_DB_CONNECTION_POOL_SIZE=3 ENABLE_TENANT_AND_SCOPE=true -TENANTS=dev,test -TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/" +TENANTS=dev,test,superposition +TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/oidc/login,/admin/organisations,/organisations,/organisations/switch/{organisation_id},/" SERVICE_PREFIX="" SERVICE_NAME="CAC" +AUTH_PROVIDER=DISABLED +LOCAL_ORGS=superposition +# AUTH_PROVIDER=OIDC+http://localhost:8081/realms/users +# OIDC_CLIENT_ID=superposition +# OIDC_CLIENT_SECRET=superposition_secret +# OIDC_TOKEN_ENDPOINT_FORMAT="http://localhost:8081/realms//protocol/openid-connect/token" +# OIDC_ISSUER_ENDPOINT_FORMAT="http://http://localhost:8081/realms/" +WORKER_ID=1 diff --git a/.github/workflows/ci_check_pr.yaml b/.github/workflows/ci_check_pr.yaml index a8b155b3..6b53c558 100644 --- a/.github/workflows/ci_check_pr.yaml +++ b/.github/workflows/ci_check_pr.yaml @@ -57,7 +57,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.78.0 targets: wasm32-unknown-unknown components: rustfmt, clippy @@ -110,7 +110,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.78.0 targets: wasm32-unknown-unknown components: rustfmt, clippy diff --git a/.github/workflows/ci_merge_main.yaml b/.github/workflows/ci_merge_main.yaml index 0622d976..a9fd63fb 100644 --- a/.github/workflows/ci_merge_main.yaml +++ b/.github/workflows/ci_merge_main.yaml @@ -22,7 +22,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@master with: - toolchain: 1.76.0 + toolchain: 1.78.0 targets: wasm32-unknown-unknown components: rustfmt, clippy diff --git a/.github/workflows/example_docker_image_gen.yaml b/.github/workflows/example_docker_image_gen.yaml index db46b28c..c6da9a79 100644 --- a/.github/workflows/example_docker_image_gen.yaml +++ b/.github/workflows/example_docker_image_gen.yaml @@ -26,8 +26,8 @@ jobs: id: git_tag shell: bash run: | - docker_tag=`git tag -l --sort=-creatordate | grep "^v" | head -n 1 | sed 's/^v//'` - echo "docker_tag=$docker_tag" >> $GITHUB_OUTPUT + docker_tag=`git tag -l --sort=-creatordate | grep "^v" | head -n 1 | sed 's/^v//'` + echo "docker_tag=$docker_tag" >> $GITHUB_OUTPUT - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/Cargo.lock b/Cargo.lock index 70d0547f..6a645d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -814,6 +814,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" @@ -836,6 +848,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -1022,7 +1040,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -1072,7 +1090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" dependencies = [ "clap 3.2.25", - "heck", + "heck 0.4.1", "indexmap 1.9.3", "log", "proc-macro2", @@ -1152,7 +1170,7 @@ dependencies = [ "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap 1.9.3", - "strsim", + "strsim 0.10.0", "termcolor", "textwrap", ] @@ -1178,7 +1196,7 @@ dependencies = [ "anstyle", "bitflags 1.3.2", "clap_lex 0.5.0", - "strsim", + "strsim 0.10.0", ] [[package]] @@ -1187,7 +1205,7 @@ version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.48", @@ -1254,6 +1272,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_format" version = "0.2.31" @@ -1407,6 +1431,18 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1417,6 +1453,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "cxx" version = "1.0.94" @@ -1467,8 +1530,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -1481,21 +1554,46 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.48", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.48", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -1509,6 +1607,27 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derive-where" version = "1.2.5" @@ -1535,15 +1654,15 @@ dependencies = [ [[package]] name = "diesel" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" +version = "2.2.4" +source = "git+https://github.com/juspay/diesel.git?branch=dynamic-schema#19833ad9e1cb2eb681b9ac9bd664063e57889414" dependencies = [ "bigdecimal", "bitflags 2.3.1", "byteorder", "chrono", "diesel_derives", + "downcast-rs", "itoa", "num-bigint", "num-integer", @@ -1560,7 +1679,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b10c03b954333d05bfd5be1d8a74eae2c9ca77b86e0f1c3a1ea29c49da1d6c2" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -1568,11 +1687,11 @@ dependencies = [ [[package]] name = "diesel_derives" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" +version = "2.2.0" +source = "git+https://github.com/juspay/diesel.git?branch=dynamic-schema#19833ad9e1cb2eb681b9ac9bd664063e57889414" dependencies = [ "diesel_table_macro_syntax", + "dsl_auto_type", "proc-macro2", "quote", "syn 2.0.48", @@ -1580,20 +1699,20 @@ dependencies = [ [[package]] name = "diesel_table_macro_syntax" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +version = "0.2.0" +source = "git+https://github.com/juspay/diesel.git?branch=dynamic-schema#19833ad9e1cb2eb681b9ac9bd664063e57889414" dependencies = [ "syn 2.0.48", ] [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1604,18 +1723,102 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "drain_filter_polyfill" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dsl_auto_type" +version = "0.1.0" +source = "git+https://github.com/juspay/diesel.git?branch=dynamic-schema#19833ad9e1cb2eb681b9ac9bd664063e57889414" +dependencies = [ + "darling 0.20.10", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1752,6 +1955,22 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.0.26" @@ -1973,6 +2192,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2034,6 +2254,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.24" @@ -2087,6 +2318,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -2117,6 +2354,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2298,6 +2544,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idgenerator" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab32f68e287887b5f783055dac63971ae26c76be1ebde166c64d5bf5bdd5b6a" +dependencies = [ + "chrono", + "once_cell", + "parking_lot", + "thiserror", +] + [[package]] name = "idna" version = "0.5.0" @@ -2322,6 +2580,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -2332,6 +2591,7 @@ checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", "hashbrown 0.14.2", + "serde", ] [[package]] @@ -2486,6 +2746,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "leptos" @@ -2722,6 +2985,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "linear-map" version = "1.2.0" @@ -2966,6 +3235,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-cmp" version = "0.1.0" @@ -2981,6 +3267,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -3021,6 +3313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3033,6 +3326,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom", + "http 0.2.9", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.30.4" @@ -3048,6 +3361,38 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openidconnect" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590" +dependencies = [ + "base64 0.13.1", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 0.2.9", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand", + "rsa", + "serde", + "serde-value", + "serde_derive", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror", + "url", +] + [[package]] name = "openssl" version = "0.10.54" @@ -3092,6 +3437,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -3104,6 +3458,30 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "pad-adapter" version = "0.1.1" @@ -3145,6 +3523,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3228,12 +3615,39 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -3259,6 +3673,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3481,6 +3904,7 @@ dependencies = [ "http 0.2.9", "http-body 0.4.5", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -3490,19 +3914,33 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -3539,6 +3977,26 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rstml" version = "0.11.2" @@ -3692,6 +4150,20 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.1" @@ -3748,6 +4220,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -3790,6 +4272,25 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_qs" version = "0.12.0" @@ -3831,6 +4332,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.2", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "server_fn" version = "0.6.11" @@ -3944,6 +4475,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "skeptic" version = "0.13.7" @@ -4016,12 +4557,28 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.25.0" @@ -4034,7 +4591,7 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -4053,22 +4610,32 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-web", - "cac_toml", + "anyhow", + "aws-sdk-kms", "cfg-if", + "chrono", "context_aware_config", + "diesel", "dotenv", "env_logger", "experimentation_platform", "fred", "frontend", + "futures-util", + "idgenerator", "leptos", "leptos_actix", + "log", + "openidconnect", + "regex", "reqwest", "rs-snowflake", + "serde", "serde_json", "service_utils", + "superposition_macros", "superposition_types", - "toml 0.8.8", + "url", ] [[package]] @@ -4111,6 +4678,7 @@ dependencies = [ "regex", "serde", "serde_json", + "strum_macros", "superposition_derives", "thiserror", "uuid", @@ -4275,11 +4843,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ + "deranged", "itoa", + "num-conv", + "powerfmt", "serde", "time-core", "time-macros", @@ -4287,16 +4858,17 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ + "num-conv", "time-core", ] @@ -4585,6 +5157,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4784,6 +5357,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "winapi" version = "0.3.9" @@ -5076,9 +5655,9 @@ checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" [[package]] name = "zeroize" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zstd" diff --git a/Cargo.toml b/Cargo.toml index 3e216cf5..bafb32eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,13 @@ assets-dir = "crates/frontend/assets" [workspace.dependencies] actix-web = "4.5.0" anyhow = "1.0.75" +aws-sdk-kms = { version = "1.38.0" } base64 = "0.21.2" -bigdecimal = { version = "0.3.1" , features= ["serde"]} +bigdecimal = { version = "0.3.1", features = ["serde"] } cfg-if = "1.0.0" chrono = { version = "0.4.26", features = ["serde"] } derive_more = "^0.99" -diesel = { version = "2.1.0", features = [ +diesel = { git = "https://github.com/juspay/diesel.git", branch = "dynamic-schema", features = [ "postgres", "r2d2", "serde_json", @@ -46,6 +47,7 @@ diesel = { version = "2.1.0", features = [ "postgres_backend", ] } fred = { version = "9.2.1" } +futures-util = "0.3.28" itertools = { version = "0.10.5" } jsonlogic = { git = "https://github.com/juspay/jsonlogic_rs.git", version = "0.5.3" } jsonschema = "~0.17" @@ -60,6 +62,7 @@ serde_json = { version = "1.0" } strum = "0.25" strum_macros = "0.25" toml = { version = "0.8.8", features = ["preserve_order"] } +url = "2.5.0" uuid = { version = "1.3.4", features = ["v4", "serde"] } [workspace.lints.clippy] diff --git a/Dockerfile b/Dockerfile index dbbe35c5..c53fe93d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.76.0 as builder +FROM rust:1.78.0 as builder WORKDIR /build @@ -42,6 +42,8 @@ RUN ls -l target FROM debian:bookworm-slim as runtime +RUN mkdir -p /app/crates/superposition +COPY --from=builder /build/crates/superposition/Superposition.cac.toml /app/crates/superposition/Superposition.cac.toml ENV NODE_VERSION=18.19.0 WORKDIR /app @@ -64,6 +66,7 @@ COPY --from=builder /build/target/release/superposition /app/superposition COPY --from=builder /build/Cargo.toml /app/Cargo.toml COPY --from=builder /build/target/site /app/target/site COPY --from=builder /build/target/node_modules /app/target/node_modules +COPY --from=builder /build/workspace_template.sql /app/workspace_template.sql # COPY --from=builder /build/superposition/target/.env /app/.env ENV SUPERPOSITION_VERSION=$SUPERPOSITION_VERSION ENV SOURCE_COMMIT=$SOURCE_COMMIT diff --git a/crates/context_aware_config/Cargo.toml b/crates/context_aware_config/Cargo.toml index 69f9ec64..344d623c 100644 --- a/crates/context_aware_config/Cargo.toml +++ b/crates/context_aware_config/Cargo.toml @@ -17,9 +17,9 @@ cac_client = { path = "../cac_client" } cfg-if = { workspace = true } chrono = { workspace = true } derive_more = { workspace = true } -diesel = { workspace = true , features = ["numeric"]} +diesel = { workspace = true, features = ["numeric"] } fred = { workspace = true, optional = true, features = ["metrics"] } -futures-util = "0.3.28" +futures-util = { workspace = true } itertools = { workspace = true } jsonlogic = { workspace = true } jsonschema = { workspace = true } @@ -33,7 +33,6 @@ superposition_macros = { path = "../superposition_macros" } superposition_types = { path = "../superposition_types", features = [ "result", "diesel_derives", - "server", ] } uuid = { workspace = true } diff --git a/crates/context_aware_config/samples/default_config.json b/crates/context_aware_config/samples/default_config.json deleted file mode 100644 index 2a21b3b4..00000000 --- a/crates/context_aware_config/samples/default_config.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "logs_endpoint": "", - "logs_pusher_timer": "", - "logs_memory_required": "", - "logs_encryption_level": "", - "logs_should_push": "", - "logs_public_key": "", - "headurl": "", - "godel_webview_access": true, - "ui_blurBackground": true, - "ui_nbListItemCaching": true, - "ui_nbList_bgCacheCapacity":4, - "useCommits": true, - "preRenderConfig": "1.0", - "safemode": true, - "urls_api": "", - "urls_endUrl": [], - "urls_webReleaseDomain": "", - "urls_assets": "", - - - "app_list": [ - "in.juspay.dotp", - "in.juspay.ec", - "in.juspay.escrow", - "in.juspay.flyer", - "in.juspay.hyperos", - "in.juspay.hyperos.placeholder", - "in.juspay.hyperpay", - "in.juspay.upiintent" - ], - - "package_version": "1.0.0", - "package": { - "in.juspay.hyperpay": "https://sandbox.assets.juspay.in/hyper/bundles/app/in.juspay.hyperpay/main/common/android/v1-index_bundle.zip", - "in.juspay.upiintent": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.upiintent/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.inappupi": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.inappupi/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.hyperupi": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.hyperupi/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.ec": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.ec/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.dotp": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.dotp/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.flyer": "https://sandbox.assets.juspay.in/juspay/payments/in.juspay.flyer/beta/2.0rc1/v1-index_bundle.zip", - "in.juspay.escrow": "https://sandbox.assets.juspay.in/hyper/bundles/web/beta/in.juspay.escrow/2.0.0/common/stable/index.js" - }, - - "package_dependencies": { - "in.juspay.dotp": { - "entry": "base.html", - "root": "payments/in.juspay.dotp/" - }, - "in.juspay.ec": { - "entry": "base.html", - "required_apps": [ - "in.juspay.dotp", - "in.juspay.escrow", - "in.juspay.flyer", - "in.juspay.godel.placeholder", - "in.juspay.hyperos.placeholder", - "in.juspay.upiintent", - "in.juspay.vies" - ], - "root": "payments/in.juspay.ec/" - }, - "in.juspay.escrow": { - "entry": "base.html", - "root": "payments/in.juspay.escrow/" - }, - "in.juspay.flyer": { - "entry": "base.html", - "root": "payments/in.juspay.flyer/" - }, - "in.juspay.godel": { - "entry": "base.html", - "root": "payments/in.juspay.godel/" - }, - - "in.juspay.godel.placeholder": { - "entry": "", - "root": "payments/in.juspay.godel/" - }, - "in.juspay.hyperapi": { - "entry": "base.html", - "required_apps": [ - "in.juspay.ec" - ], - "root": "payments/in.juspay.hyperapi/" - }, - "in.juspay.hyperos": { - "entry": "", - "root": "" - }, - "in.juspay.hyperos.placeholder": { - "entry": "", - "root": "" - }, - "in.juspay.hyperpay": { - "entry": "base.html", - "required_apps": [ - "in.juspay.ec" - ], - "root": "payments/in.juspay.hyperpay/" - }, - "in.juspay.upiintent": { - - "entry": "base.html", - "root": "payments/in.juspay.upiintent/" - - }, - "in.juspay.vies": { - - "entry": "base.html", - "root": "payments/in.juspay.vies/" - - } - }, - - "package_assets": { - "in.juspay.dotp": { - "config": "https://assets.juspay.in/juspay/payments/in.juspay.dotp/release/v1-config.zip" - }, - "in.juspay.ec": { - "config": "https://assets.juspay.in/juspay/payments/in.juspay.ec/release/v1-config.zip" - }, - "in.juspay.escrow": { - "configuration": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-configuration.zip", - "icons": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-icons.zip", - "strings": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-strings.zip" - }, - "in.juspay.godel": { - "acs_js_source": "https://assets.juspay.in/juspay/payments/in.juspay.godel/release/1.0rc2/v1-acs.zip", - "boot_loader_js_source": "https://assets.juspay.in/hyper/bundles/release/in.juspay.godel/android/1.0rc2/common/stable/v1-boot_loader.zip", - "config": "https://d3e0hckk6jr53z.cloudfront.net/godel/v1-config.zip" - }, - "in.juspay.godel.placeholder": { - "acs_js_source": "https://assets.juspay.in/juspay/payments/in.juspay.godel/release/1.0rc2/v1-acs.zip", - "boot_loader_js_source": "https://assets.juspay.in/hyper/bundles/release/in.juspay.godel/android/1.0rc2/common/stable/v1-boot_loader.zip", - "config": "https://d3e0hckk6jr53z.cloudfront.net/godel/v1-config.zip" - }, - "in.juspay.hyperos": { - "config": "https://assets.juspay.in/juspay/payments/2.0/release/v1-config.zip", - "manifest": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/android/release/manifest.json" - }, - "in.juspay.hyperos.placeholder": { - "tracker": "https://assets.juspay.in/juspay/payments/2.0/release/v1-tracker.zip" - }, - "in.juspay.hyperpay": { - "configuration": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-configuration.zip", - "icons": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-icons.zip", - "strings": "https://assets.juspay.in/hyper/bundles/in.juspay.merchants/toi/configuration/1.22/v1-strings.zip" - }, - "in.juspay.upiintent": { - "config": "https://assets.juspay.in/hyper/bundles/android/release/in.juspay.upiintent/2.0.0/common/2.0.109/v1-config.zip" - }, - "in.juspay.vies": { - "config": "https://assets.juspay.in/juspay/payments/in.juspay.vies/release/1.0.89/v1-config.zip" - } - } -} diff --git a/crates/context_aware_config/samples/dimensions.json b/crates/context_aware_config/samples/dimensions.json deleted file mode 100644 index ce3617b9..00000000 --- a/crates/context_aware_config/samples/dimensions.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "dimensions": [ - { - "condition": { - "==": [{"var":"tier"}, "1"] - }, - "overrideWithKeys": ["tier1"] - - }, - { - "condition": { - "==": [{"var":"merchantId"}, "zee5"] - }, - "overrideWithKeys": ["zee5"] - } - ] - -} \ No newline at end of file diff --git a/crates/context_aware_config/samples/overrides.json b/crates/context_aware_config/samples/overrides.json deleted file mode 100644 index 1b1d09d4..00000000 --- a/crates/context_aware_config/samples/overrides.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "tier1" :{ - "package": { - "in.juspay.hyperpay": "changed hyperpay by tier 1", - "in.juspay.upiintent": "changed upiintent by tier 1" - }, - "package_dependencies": { - "in.juspay.ec": { - "entry": "changed entry of ec by tier 1", - "required_apps": [ - "in.juspay.dotp", - "in.juspay.escrow", - "in.juspay.flyer" - ] - } - }, - "package_assets" : { - "in.juspay.upiintent": { - "config": "changed upiintent config by tier 1" - } - } - - - }, - "zee5": { - "app_list": [], - "logs_should_push": "changed shoudl push by zee5", - "logs_public_key": "changed blogs public key by zee5", - "package_assets": { - "in.juspay.hyperpay": { - "configuration": "changed configuration by zee5", - "strings": "changed hyperpay strings by zee5" - }, - "in.juspay.upiintent": { - "config": "changed upintent by zee5" - }, - "new_key": { - "config" : "added by zee5" - } - } - } -} diff --git a/crates/context_aware_config/src/api/audit_log/handlers.rs b/crates/context_aware_config/src/api/audit_log/handlers.rs index bc001cfc..04a0e3f0 100644 --- a/crates/context_aware_config/src/api/audit_log/handlers.rs +++ b/crates/context_aware_config/src/api/audit_log/handlers.rs @@ -1,11 +1,14 @@ -use actix_web::{get, web::Query, HttpResponse, Scope}; +use actix_web::{ + get, + web::{Json, Query}, + Scope, +}; use chrono::{Duration, Utc}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; -use serde_json::json; use service_utils::service::types::DbConnection; use superposition_types::{ - cac::{models::EventLog, schema::event_log::dsl as event_log}, - result as superposition, + database::{models::cac::EventLog, schema::event_log::dsl as event_log}, + result as superposition, PaginatedResponse, }; use crate::api::audit_log::types::AuditQueryFilters; @@ -18,7 +21,7 @@ pub fn endpoints() -> Scope { async fn get_audit_logs( filters: Query, db_conn: DbConnection, -) -> superposition::Result { +) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; let query_builder = |filters: &AuditQueryFilters| { @@ -57,9 +60,9 @@ async fn get_audit_logs( let total_pages = (log_count as f64 / limit as f64).ceil() as i64; - Ok(HttpResponse::Ok().json(json!({ - "total_items": log_count, - "total_pages": total_pages, - "data": logs - }))) + Ok(Json(PaginatedResponse { + total_items: log_count, + total_pages, + data: logs, + })) } diff --git a/crates/context_aware_config/src/api/config/handlers.rs b/crates/context_aware_config/src/api/config/handlers.rs index 09c3ca2a..63dea913 100644 --- a/crates/context_aware_config/src/api/config/handlers.rs +++ b/crates/context_aware_config/src/api/config/handlers.rs @@ -14,17 +14,14 @@ use actix_web::{ }; use cac_client::{eval_cac, eval_cac_with_reasoning, MergeStrategy}; use chrono::{DateTime, NaiveDateTime, TimeZone, Timelike, Utc}; -use diesel::{ - dsl::max, - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; +use diesel::{dsl::max, ExpressionMethods, QueryDsl, RunQueryDsl}; #[cfg(feature = "high-performance-mode")] use fred::interfaces::KeysInterface; use itertools::Itertools; use serde_json::{json, Map, Value}; #[cfg(feature = "high-performance-mode")] -use service_utils::service::types::{AppState, Tenant}; +use service_utils::service::types::AppState; +use service_utils::service::types::Tenant; use service_utils::{ helpers::extract_dimensions, service::types::{AppHeader, DbConnection}, @@ -33,23 +30,20 @@ use service_utils::{ use superposition_macros::response_error; use superposition_macros::{bad_argument, db_error, unexpected_error}; use superposition_types::{ - cac::{ - models::ConfigVersion, - schema::{config_versions::dsl as config_versions, event_log::dsl as event_log}, - }, custom_query::{ self as superposition_query, CustomQuery, PaginationParams, QueryMap, }, - result as superposition, Cac, Condition, Config, Context, Overrides, + database::{ + models::cac::ConfigVersion, + schema::{config_versions::dsl as config_versions, event_log::dsl as event_log}, + }, + result as superposition, Cac, Condition, Config, Context, DBConnection, Overrides, PaginatedResponse, TenantConfig, User, }; use uuid::Uuid; -use crate::helpers::generate_cac; -use crate::{ - api::context::{delete_context_api, hash, put, PutReq}, - helpers::DimensionData, -}; +use crate::{api::context, helpers::DimensionData}; +use crate::{api::context::PutReq, helpers::generate_cac}; use crate::{ api::dimension::{get_dimension_data, get_dimension_data_map}, helpers::calculate_context_weight, @@ -85,13 +79,15 @@ fn validate_version_in_params( } pub fn add_audit_id_to_header( - conn: &mut PooledConnection>, + conn: &mut DBConnection, resp_builder: &mut HttpResponseBuilder, + tenant: &Tenant, ) { if let Ok(uuid) = event_log::event_log .select(event_log::id) .filter(event_log::table_name.eq("contexts")) .order_by(event_log::timestamp.desc()) + .schema_name(tenant) .first::(conn) { resp_builder.insert_header((AppHeader::XAuditId.to_string(), uuid.to_string())); @@ -129,11 +125,13 @@ fn add_config_version_to_header( } fn get_max_created_at( - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> Result { event_log::event_log .select(max(event_log::timestamp)) .filter(event_log::table_name.eq_any(vec!["contexts", "default_configs"])) + .schema_name(tenant) .first::>(conn) .and_then(|res| res.ok_or(diesel::result::Error::NotFound)) } @@ -157,13 +155,15 @@ fn is_not_modified(max_created_at: Option, req: &HttpRequest) -> pub fn generate_config_from_version( version: &mut Option, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { if let Some(val) = version { let val = val.clone(); let config = config_versions::config_versions .select(config_versions::config) .filter(config_versions::id.eq(val)) + .schema_name(tenant) .get_result::(conn) .map_err(|err| { log::error!("failed to fetch config with error: {}", err); @@ -177,18 +177,19 @@ pub fn generate_config_from_version( match config_versions::config_versions .select((config_versions::id, config_versions::config)) .order(config_versions::created_at.desc()) + .schema_name(tenant) .first::<(i64, Value)>(conn) { Ok((latest_version, config)) => { *version = Some(latest_version); serde_json::from_value::(config).or_else(|err| { log::error!("failed to decode config: {}", err); - generate_cac(conn) + generate_cac(conn, tenant) }) } Err(err) => { log::error!("failed to find latest config: {err}"); - generate_cac(conn) + generate_cac(conn, tenant) } } } @@ -342,7 +343,7 @@ fn get_contextids_from_overrideid( fn construct_new_payload( req_payload: &Map, -) -> superposition::Result> { +) -> superposition::Result> { let mut res = req_payload.clone(); res.remove("to_be_deleted"); res.remove("override_id"); @@ -380,16 +381,37 @@ fn construct_new_payload( }, )?; - return Ok(web::Json(PutReq { - context: context, + let description = match res.get("description") { + Some(Value::String(s)) => Some(s.clone()), + Some(_) => { + log::error!("construct new payload: Description is not a valid string"); + return Err(bad_argument!("Description must be a string")); + } + None => None, + }; + + // Handle change_reason + let change_reason = res + .get("change_reason") + .and_then(|val| val.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| { + log::error!("construct new payload: Change reason not present or invalid"); + bad_argument!("Change reason is required and must be a string") + })?; + + Ok(web::Json(PutReq { + context, r#override: override_, - })); + description, + change_reason, + })) } #[allow(clippy::too_many_arguments)] async fn reduce_config_key( user: User, - conn: &mut PooledConnection>, + conn: &mut DBConnection, tenant_config: &TenantConfig, mut og_contexts: Vec, mut og_overrides: HashMap, @@ -397,6 +419,7 @@ async fn reduce_config_key( dimension_schema_map: &HashMap, default_config: Map, is_approve: bool, + tenant: Tenant, ) -> superposition::Result { let default_config_val = default_config @@ -467,19 +490,37 @@ async fn reduce_config_key( if *to_be_deleted { if is_approve { - let _ = delete_context_api(cid.clone(), user.clone(), conn); + let _ = context::delete( + cid.clone(), + user.clone(), + conn, + tenant.clone(), + ); } og_contexts.retain(|x| x.id != *cid); } else { if is_approve { - let _ = delete_context_api(cid.clone(), user.clone(), conn); + let _ = context::delete( + cid.clone(), + user.clone(), + conn, + tenant.clone(), + ); if let Ok(put_req) = construct_new_payload(request_payload) { - let _ = - put(put_req, conn, false, &user, &tenant_config, false); + let _ = context::put( + put_req, + conn, + false, + &user, + tenant.clone(), + &tenant_config, + false, + ); } } - let new_id = hash(&Value::Object(override_val.clone().into())); + let new_id = + context::hash(&Value::Object(override_val.clone().into())); og_overrides.insert(new_id.clone(), override_val); let mut ctx_index = 0; @@ -519,6 +560,7 @@ async fn reduce_config( user: User, db_conn: DbConnection, tenant_config: TenantConfig, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let is_approve = req @@ -527,9 +569,9 @@ async fn reduce_config( .and_then(|value| value.to_str().ok().and_then(|s| s.parse::().ok())) .unwrap_or(false); - let dimensions_vec = get_dimension_data(&mut conn)?; + let dimensions_vec = get_dimension_data(&mut conn, &tenant)?; let dimensions_data_map = get_dimension_data_map(&dimensions_vec)?; - let mut config = generate_cac(&mut conn)?; + let mut config = generate_cac(&mut conn, &tenant)?; let default_config = (config.default_configs).clone(); for (key, _) in default_config { let contexts = config.contexts; @@ -545,10 +587,11 @@ async fn reduce_config( &dimensions_data_map, default_config.clone(), is_approve, + tenant.clone(), ) .await?; if is_approve { - config = generate_cac(&mut conn)?; + config = generate_cac(&mut conn, &tenant)?; } } @@ -649,10 +692,11 @@ async fn get_config( req: HttpRequest, db_conn: DbConnection, query_map: superposition_query::Query, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; - let max_created_at = get_max_created_at(&mut conn) + let max_created_at = get_max_created_at(&mut conn, &tenant) .map_err(|e| log::error!("failed to fetch max timestamp from event_log: {e}")) .ok(); @@ -666,7 +710,8 @@ async fn get_config( let mut query_params_map = query_map.into_inner(); let mut config_version = validate_version_in_params(&mut query_params_map)?; - let mut config = generate_config_from_version(&mut config_version, &mut conn)?; + let mut config = + generate_config_from_version(&mut config_version, &mut conn, &tenant)?; config = apply_prefix_filter_to_config(&mut query_params_map, config)?; @@ -676,7 +721,7 @@ async fn get_config( let mut response = HttpResponse::Ok(); add_last_modified_to_header(max_created_at, &mut response); - add_audit_id_to_header(&mut conn, &mut response); + add_audit_id_to_header(&mut conn, &mut response, &tenant); add_config_version_to_header(&config_version, &mut response); Ok(response.json(config)) } @@ -686,11 +731,12 @@ async fn get_resolved_config( req: HttpRequest, db_conn: DbConnection, query_map: superposition_query::Query, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let mut query_params_map = query_map.into_inner(); - let max_created_at = get_max_created_at(&mut conn) + let max_created_at = get_max_created_at(&mut conn, &tenant) .map_err(|e| log::error!("failed to fetch max timestamp from event_log : {e}")) .ok(); @@ -701,7 +747,8 @@ async fn get_resolved_config( } let mut config_version = validate_version_in_params(&mut query_params_map)?; - let mut config = generate_config_from_version(&mut config_version, &mut conn)?; + let mut config = + generate_config_from_version(&mut config_version, &mut conn, &tenant)?; config = apply_prefix_filter_to_config(&mut query_params_map, config)?; @@ -745,7 +792,7 @@ async fn get_resolved_config( }; let mut resp = HttpResponse::Ok(); add_last_modified_to_header(max_created_at, &mut resp); - add_audit_id_to_header(&mut conn, &mut resp); + add_audit_id_to_header(&mut conn, &mut resp, &tenant); add_config_version_to_header(&config_version, &mut resp); Ok(resp.json(response)) @@ -755,12 +802,14 @@ async fn get_resolved_config( async fn get_config_versions( db_conn: DbConnection, filters: Query, + tenant: Tenant, ) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; if let Some(true) = filters.all { - let config_versions: Vec = - config_versions::config_versions.get_results(&mut conn)?; + let config_versions: Vec = config_versions::config_versions + .schema_name(&tenant) + .get_results(&mut conn)?; return Ok(Json(PaginatedResponse { total_pages: 1, total_items: config_versions.len() as i64, @@ -770,10 +819,12 @@ async fn get_config_versions( let n_version: i64 = config_versions::config_versions .count() + .schema_name(&tenant) .get_result(&mut conn)?; let limit = filters.count.unwrap_or(10); let mut builder = config_versions::config_versions + .schema_name(&tenant) .into_boxed() .order(config_versions::created_at.desc()) .limit(limit); diff --git a/crates/context_aware_config/src/api/context.rs b/crates/context_aware_config/src/api/context.rs index a85ac665..d94add87 100644 --- a/crates/context_aware_config/src/api/context.rs +++ b/crates/context_aware_config/src/api/context.rs @@ -1,9 +1,11 @@ mod handlers; pub mod helpers; +pub mod operations; mod types; -pub use handlers::delete_context_api; +pub mod validations; pub use handlers::endpoints; -pub use handlers::hash; -pub use handlers::put; -pub use handlers::validate_dimensions; +pub use helpers::hash; +pub use operations::delete; +pub use operations::put; pub use types::PutReq; +pub use validations::validate_dimensions; diff --git a/crates/context_aware_config/src/api/context/handlers.rs b/crates/context_aware_config/src/api/context/handlers.rs index b0454b50..97e4b787 100644 --- a/crates/context_aware_config/src/api/context/handlers.rs +++ b/crates/context_aware_config/src/api/context/handlers.rs @@ -1,9 +1,25 @@ extern crate base64; -use std::collections::HashMap; -use std::str; -use std::{cmp::min, collections::HashSet}; +use std::{ + cmp::min, + collections::{HashMap, HashSet}, +}; +#[cfg(feature = "high-performance-mode")] +use crate::helpers::put_config_in_redis; +use crate::{ + api::{ + context::types::{ + ContextAction, ContextBulkResponse, ContextFilterSortOn, ContextFilters, + MoveReq, PutReq, WeightRecomputeResponse, + }, + dimension::{get_dimension_data, get_dimension_data_map}, + }, + helpers::{ + add_config_version, calculate_context_weight, validate_context_jsonschema, + DimensionData, + }, +}; use actix_web::{ delete, get, post, put, web::{Data, Json, Path}, @@ -12,55 +28,41 @@ use actix_web::{ use bigdecimal::BigDecimal; use cac_client::utils::json_to_sorted_string; use chrono::Utc; +use diesel::result::Error::DatabaseError; use diesel::{ delete, r2d2::{ConnectionManager, PooledConnection}, - result::{DatabaseErrorKind::*, Error::DatabaseError}, Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, }; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{from_value, json, Map, Value}; -#[cfg(feature = "high-performance-mode")] -use service_utils::service::types::Tenant; use service_utils::{ - helpers::{parse_config_tags, validation_err_to_str}, + helpers::parse_config_tags, service::types::{AppHeader, AppState, CustomHeaders, DbConnection}, }; +use service_utils::{helpers::validation_err_to_str, service::types::Tenant}; use superposition_macros::{ bad_argument, db_error, not_found, unexpected_error, validation_error, }; use superposition_types::{ - cac::{ - models::Context, + custom_query::{self as superposition_query, CustomQuery, DimensionQuery, QueryMap}, + database::{ + models::cac::Context, schema::{ contexts::{self, id}, default_configs::dsl, }, }, - custom_query::{self as superposition_query, CustomQuery, DimensionQuery, QueryMap}, result as superposition, Cac, Contextual, Overridden, Overrides, PaginatedResponse, SortBy, TenantConfig, User, }; -#[cfg(feature = "high-performance-mode")] -use crate::helpers::put_config_in_redis; -use crate::{ - api::{ - context::types::{ - ContextAction, ContextBulkResponse, ContextFilterSortOn, ContextFilters, - DimensionCondition, MoveReq, PutReq, PutResp, WeightRecomputeResponse, - }, - dimension::{get_dimension_data, get_dimension_data_map}, - }, +use super::{ helpers::{ - add_config_version, calculate_context_weight, validate_context_jsonschema, - DimensionData, + validate_condition_with_functions, validate_condition_with_mandatory_dimensions, + validate_override_with_functions, }, -}; - -use super::helpers::{ - validate_condition_with_functions, validate_condition_with_mandatory_dimensions, - validate_override_with_functions, + types::{DimensionCondition, PutResp}, }; pub fn endpoints() -> Scope { @@ -68,7 +70,7 @@ pub fn endpoints() -> Scope { .service(put_handler) .service(update_override_handler) .service(move_handler) - .service(delete_context) + .service(delete_context_handler) .service(bulk_operations) .service(list_contexts) .service(get_context_from_condition) @@ -149,11 +151,13 @@ pub fn validate_dimensions( fn validate_override_with_default_configs( conn: &mut DBConnection, override_: &Map, + tenant: &Tenant, ) -> superposition::Result<()> { let keys_array: Vec<&String> = override_.keys().collect(); let res: Vec<(String, Value)> = dsl::default_configs - .filter(dsl::key.eq_any(keys_array)) .select((dsl::key, dsl::schema)) + .filter(dsl::key.eq_any(keys_array)) + .schema_name(tenant) .get_results::<(String, Value)>(conn)?; let map = Map::from_iter(res); @@ -196,8 +200,18 @@ fn create_ctx_from_put_req( conn: &mut DBConnection, user: &User, tenant_config: &TenantConfig, + tenant: &Tenant, ) -> superposition::Result { let ctx_condition = req.context.to_owned().into_inner(); + let description = if req.description.is_none() { + let ctx_condition_value = json!(ctx_condition); + ensure_description(ctx_condition_value, conn, tenant)? + } else { + req.description + .clone() + .ok_or_else(|| bad_argument!("Description should not be empty"))? + }; + let change_reason = req.change_reason.clone(); let condition_val = Value::Object(ctx_condition.clone().into()); let r_override = req.r#override.clone().into_inner(); let ctx_override = Value::Object(r_override.clone().into()); @@ -205,11 +219,11 @@ fn create_ctx_from_put_req( &ctx_condition, &tenant_config.mandatory_dimensions, )?; - validate_override_with_default_configs(conn, &r_override)?; - validate_condition_with_functions(conn, &ctx_condition)?; - validate_override_with_functions(conn, &r_override)?; + validate_override_with_default_configs(conn, &r_override, tenant)?; + validate_condition_with_functions(conn, &ctx_condition, tenant)?; + validate_override_with_functions(conn, &r_override, tenant)?; - let dimension_data = get_dimension_data(conn)?; + let dimension_data = get_dimension_data(conn, tenant)?; let dimension_data_map = get_dimension_data_map(&dimension_data)?; validate_dimensions("context", &condition_val, &dimension_data_map)?; @@ -228,6 +242,8 @@ fn create_ctx_from_put_req( last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), weight, + description: description, + change_reason, }) } @@ -240,11 +256,13 @@ fn update_override_of_existing_ctx( conn: &mut PooledConnection>, ctx: Context, user: &User, + tenant: &Tenant, ) -> superposition::Result { use contexts::dsl; let mut new_override: Value = dsl::contexts - .filter(dsl::id.eq(ctx.id.clone())) .select(dsl::override_) + .filter(dsl::id.eq(ctx.id.clone())) + .schema_name(tenant) .first(conn)?; cac_client::merge( &mut new_override, @@ -266,13 +284,14 @@ fn update_override_of_existing_ctx( override_id: new_override_id, ..ctx }; - db_update_override(conn, new_ctx, user) + db_update_override(conn, new_ctx, user, tenant) } fn replace_override_of_existing_ctx( conn: &mut PooledConnection>, ctx: Context, user: &User, + tenant: &Tenant, ) -> superposition::Result { let new_override = ctx.override_; let new_override_id = hash(&Value::Object(new_override.clone().into())); @@ -281,13 +300,14 @@ fn replace_override_of_existing_ctx( override_id: new_override_id, ..ctx }; - db_update_override(conn, new_ctx, user) + db_update_override(conn, new_ctx, user, tenant) } fn db_update_override( conn: &mut PooledConnection>, ctx: Context, user: &User, + tenant: &Tenant, ) -> superposition::Result { use contexts::dsl; let update_resp = diesel::update(dsl::contexts) @@ -297,7 +317,10 @@ fn db_update_override( dsl::override_id.eq(ctx.override_id), dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.get_email()), + dsl::description.eq(ctx.description), + dsl::change_reason.eq(ctx.change_reason), )) + .schema_name(tenant) .get_result::(conn)?; Ok(get_put_resp(update_resp)) } @@ -307,6 +330,8 @@ fn get_put_resp(ctx: Context) -> PutResp { context_id: ctx.id, override_id: ctx.override_id, weight: ctx.weight, + description: ctx.description, + change_reason: ctx.change_reason, } } @@ -317,25 +342,28 @@ pub fn put( user: &User, tenant_config: &TenantConfig, replace: bool, + tenant: &Tenant, ) -> superposition::Result { use contexts::dsl::contexts; - let new_ctx = create_ctx_from_put_req(req, conn, user, tenant_config)?; - + let new_ctx = create_ctx_from_put_req(req, conn, user, tenant_config, tenant)?; if already_under_txn { diesel::sql_query("SAVEPOINT put_ctx_savepoint").execute(conn)?; } - let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); + let insert = diesel::insert_into(contexts) + .values(&new_ctx) + .schema_name(tenant) + .execute(conn); match insert { Ok(_) => Ok(get_put_resp(new_ctx)), - Err(DatabaseError(UniqueViolation, _)) => { + Err(DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _)) => { if already_under_txn { diesel::sql_query("ROLLBACK TO put_ctx_savepoint").execute(conn)?; } if replace { - replace_override_of_existing_ctx(conn, new_ctx, user) // no need for .map(Json) + replace_override_of_existing_ctx(conn, new_ctx, user, tenant) // no need for .map(Json) } else { - update_override_of_existing_ctx(conn, new_ctx, user) + update_override_of_existing_ctx(conn, new_ctx, user, tenant) } } Err(e) => { @@ -345,6 +373,35 @@ pub fn put( } } +fn ensure_description( + context: Value, + transaction_conn: &mut diesel::PgConnection, + tenant: &Tenant, +) -> Result { + use superposition_types::database::schema::contexts::dsl::{ + contexts as contexts_table, id as context_id, + }; + + let context_id_value = hash(&context); + + // Perform the database query + let existing_context = contexts_table + .filter(context_id.eq(context_id_value)) + .schema_name(tenant) + .first::(transaction_conn); + + match existing_context { + Ok(ctx) => Ok(ctx.description), // If the context is found, return the description + Err(diesel::result::Error::NotFound) => Err(superposition::AppError::NotFound( + "Description not found in the existing context".to_string(), + )), + Err(e) => { + log::error!("Database error while fetching context: {:?}", e); + Err(superposition::AppError::DbError(e)) // Use the `DbError` variant for other Diesel-related errors + } + } +} + #[put("")] async fn put_handler( state: Data, @@ -352,22 +409,50 @@ async fn put_handler( req: Json, mut db_conn: DbConnection, user: User, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; + let (put_response, version_id) = db_conn .transaction::<_, superposition::AppError, _>(|transaction_conn| { - let put_response = - put(req, transaction_conn, true, &user, &tenant_config, false).map_err( - |err: superposition::AppError| { - log::info!("context put failed with error: {:?}", err); - err - }, - )?; - let version_id = add_config_version(&state, tags, transaction_conn)?; + let mut req_mut = req.into_inner(); + + // Use the helper function to ensure the description + if req_mut.description.is_none() { + req_mut.description = Some(ensure_description( + Value::Object(req_mut.context.clone().into_inner().into()), + transaction_conn, + &tenant, + )?); + } + let put_response = put( + Json(req_mut.clone()), + transaction_conn, + true, + &user, + &tenant_config, + false, + &tenant, + ) + .map_err(|err: superposition::AppError| { + log::info!("context put failed with error: {:?}", err); + err + })?; + let description = req_mut.description.unwrap_or_default(); + let change_reason = req_mut.change_reason; + + let version_id = add_config_version( + &state, + tags, + description, + change_reason, + transaction_conn, + &tenant, + )?; Ok((put_response, version_id)) })?; + let mut http_resp = HttpResponse::Ok(); http_resp.insert_header(( @@ -390,20 +475,41 @@ async fn update_override_handler( req: Json, mut db_conn: DbConnection, user: User, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; let (override_resp, version_id) = db_conn .transaction::<_, superposition::AppError, _>(|transaction_conn| { - let override_resp = - put(req, transaction_conn, true, &user, &tenant_config, true).map_err( - |err: superposition::AppError| { - log::info!("context put failed with error: {:?}", err); - err - }, - )?; - let version_id = add_config_version(&state, tags, transaction_conn)?; + let mut req_mut = req.into_inner(); + if req_mut.description.is_none() { + req_mut.description = Some(ensure_description( + Value::Object(req_mut.context.clone().into_inner().into()), + transaction_conn, + &tenant, + )?); + } + let override_resp = put( + Json(req_mut.clone()), + transaction_conn, + true, + &user, + &tenant_config, + true, + &tenant, + ) + .map_err(|err: superposition::AppError| { + log::info!("context put failed with error: {:?}", err); + err + })?; + let version_id = add_config_version( + &state, + tags, + req_mut.description.unwrap().clone(), + req_mut.change_reason.clone(), + transaction_conn, + &tenant, + )?; Ok((override_resp, version_id)) })?; let mut http_resp = HttpResponse::Ok(); @@ -428,14 +534,26 @@ fn r#move( already_under_txn: bool, user: &User, tenant_config: &TenantConfig, + tenant: &Tenant, ) -> superposition::Result { use contexts::dsl; let req = req.into_inner(); + + let ctx_condition = req.context.to_owned().into_inner(); + let ctx_condition_value = Value::Object(ctx_condition.clone().into()); + let description = if req.description.is_none() { + ensure_description(ctx_condition_value.clone(), conn, tenant)? + } else { + req.description + .ok_or_else(|| bad_argument!("Description should not be empty"))? + }; + + let change_reason = req.change_reason.clone(); let ctx_condition = req.context.to_owned().into_inner(); let ctx_condition_value = Value::Object(ctx_condition.clone().into()); let new_ctx_id = hash(&ctx_condition_value); - let dimension_data = get_dimension_data(conn)?; + let dimension_data = get_dimension_data(conn, &tenant)?; let dimension_data_map = get_dimension_data_map(&dimension_data)?; validate_dimensions("context", &ctx_condition_value, &dimension_data_map)?; let weight = calculate_context_weight(&ctx_condition_value, &dimension_data_map) @@ -471,6 +589,8 @@ fn r#move( last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), weight, + description: description, + change_reason, }; let handle_unique_violation = @@ -478,24 +598,26 @@ fn r#move( if already_under_txn { let deleted_ctxt = diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) + .schema_name(tenant) .get_result(db_conn)?; let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); - update_override_of_existing_ctx(db_conn, ctx, user) + update_override_of_existing_ctx(db_conn, ctx, user, tenant) } else { db_conn.transaction(|conn| { let deleted_ctxt = diesel::delete(dsl::contexts) .filter(dsl::id.eq(&old_ctx_id)) + .schema_name(tenant) .get_result(conn)?; let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); - update_override_of_existing_ctx(conn, ctx, user) + update_override_of_existing_ctx(conn, ctx, user, tenant) }) } }; match context { Ok(ctx) => Ok(get_put_resp(ctx)), - Err(DatabaseError(UniqueViolation, _)) => { + Err(DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _)) => { if already_under_txn { diesel::sql_query("ROLLBACK TO update_ctx_savepoint").execute(conn)?; } @@ -516,7 +638,7 @@ async fn move_handler( req: Json, mut db_conn: DbConnection, user: User, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { let tags = parse_config_tags(custom_headers.config_tags)?; @@ -529,12 +651,21 @@ async fn move_handler( true, &user, &tenant_config, + &tenant, ) .map_err(|err| { log::info!("move api failed with error: {:?}", err); err })?; - let version_id = add_config_version(&state, tags, transaction_conn)?; + let version_id = add_config_version( + &state, + tags, + move_response.description.clone(), + move_response.change_reason.clone(), + transaction_conn, + &tenant, + )?; + Ok((move_response, version_id)) })?; let mut http_resp = HttpResponse::Ok(); @@ -556,14 +687,16 @@ async fn move_handler( async fn get_context_from_condition( db_conn: DbConnection, req: Json>, + tenant: Tenant, ) -> superposition::Result> { - use superposition_types::cac::schema::contexts::dsl::*; + use superposition_types::database::schema::contexts::dsl::*; let context_id = hash(&Value::Object(req.into_inner())); let DbConnection(mut conn) = db_conn; let ctx: Context = contexts .filter(id.eq(context_id)) + .schema_name(&tenant) .get_result::(&mut conn)?; Ok(Json(ctx)) @@ -573,14 +706,16 @@ async fn get_context_from_condition( async fn get_context( path: Path, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result> { - use superposition_types::cac::schema::contexts::dsl::*; + use superposition_types::database::schema::contexts::dsl::*; let ctx_id = path.into_inner(); let DbConnection(mut conn) = db_conn; let ctx: Context = contexts .filter(id.eq(ctx_id)) + .schema_name(&tenant) .get_result::(&mut conn)?; Ok(Json(ctx)) @@ -591,8 +726,9 @@ async fn list_contexts( filter_params: superposition_query::Query, dimension_params: DimensionQuery, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result>> { - use superposition_types::cac::schema::contexts::dsl::*; + use superposition_types::database::schema::contexts::dsl::*; let DbConnection(mut conn) = db_conn; let filter_params = filter_params.into_inner(); @@ -606,7 +742,7 @@ async fn list_contexts( } let dimension_params = dimension_params.into_inner(); - let builder = contexts.into_boxed(); + let builder = contexts.schema_name(&tenant).into_boxed(); #[rustfmt::skip] let mut builder = match (filter_params.sort_on.unwrap_or_default(), filter_params.sort_by.unwrap_or(SortBy::Asc)) { @@ -652,7 +788,7 @@ async fn list_contexts( (data, total_items as i64) } else { - let mut total_count_builder = contexts.into_boxed(); + let mut total_count_builder = contexts.schema_name(&tenant).into_boxed(); if let Some(created_bys) = filter_params.created_by { total_count_builder = total_count_builder.filter(created_by.eq_any(created_bys.0)) @@ -677,6 +813,7 @@ pub fn delete_context_api( ctx_id: String, user: User, conn: &mut PooledConnection>, + tenant: &Tenant, ) -> superposition::Result<()> { use contexts::dsl; diesel::update(dsl::contexts) @@ -685,8 +822,12 @@ pub fn delete_context_api( dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.get_email()), )) + .schema_name(tenant) .execute(conn)?; - let deleted_row = delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(conn); + let deleted_row = delete(dsl::contexts) + .filter(dsl::id.eq(&ctx_id)) + .schema_name(tenant) + .execute(conn); match deleted_row { Ok(0) => Err(not_found!("Context Id `{}` doesn't exists", ctx_id)), Ok(_) => { @@ -701,20 +842,36 @@ pub fn delete_context_api( } #[delete("/{ctx_id}")] -async fn delete_context( +async fn delete_context_handler( state: Data, path: Path, custom_headers: CustomHeaders, user: User, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, mut db_conn: DbConnection, ) -> superposition::Result { + use superposition_types::database::schema::contexts::dsl::{ + contexts as contexts_table, id as context_id, + }; let ctx_id = path.into_inner(); let tags = parse_config_tags(custom_headers.config_tags)?; let version_id = db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { - delete_context_api(ctx_id, user, transaction_conn)?; - let version_id = add_config_version(&state, tags, transaction_conn)?; + let context = contexts_table + .filter(context_id.eq(ctx_id.clone())) + .schema_name(&tenant) + .first::(transaction_conn)?; + delete_context_api(ctx_id.clone(), user.clone(), transaction_conn, &tenant)?; + let description = context.description; + let change_reason = format!("Deleted context by {}", user.username); + let version_id = add_config_version( + &state, + tags, + description, + change_reason, + transaction_conn, + &tenant, + )?; Ok(version_id) })?; cfg_if::cfg_if! { @@ -738,11 +895,14 @@ async fn bulk_operations( reqs: Json>, db_conn: DbConnection, user: User, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { use contexts::dsl::contexts; let DbConnection(mut conn) = db_conn; + let mut all_descriptions = Vec::new(); + let mut all_change_reasons = Vec::new(); + let tags = parse_config_tags(custom_headers.config_tags)?; let (response, version_id) = conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { @@ -751,12 +911,13 @@ async fn bulk_operations( match action { ContextAction::Put(put_req) => { let put_resp = put( - Json(put_req), + Json(put_req.clone()), transaction_conn, true, &user, &tenant_config, false, + &tenant, ) .map_err(|err| { log::error!( @@ -765,12 +926,43 @@ async fn bulk_operations( ); err })?; + + let ctx_condition = put_req.context.to_owned().into_inner(); + let ctx_condition_value = + Value::Object(ctx_condition.clone().into()); + + let description = if put_req.description.is_none() { + ensure_description( + ctx_condition_value.clone(), + transaction_conn, + &tenant, + )? + } else { + put_req + .description + .expect("Description should not be empty") + }; + all_descriptions.push(description); + all_change_reasons.push(put_req.change_reason.clone()); response.push(ContextBulkResponse::Put(put_resp)); } ContextAction::Delete(ctx_id) => { - let deleted_row = delete(contexts.filter(id.eq(&ctx_id))) + let context: Context = contexts + .filter(id.eq(&ctx_id)) + .schema_name(&tenant) + .first::(transaction_conn)?; + + let deleted_row = delete(contexts) + .filter(id.eq(&ctx_id)) + .schema_name(&tenant) .execute(transaction_conn); - let email: String = user.get_email(); + let description = context.description; + + let email: String = user.clone().get_email(); + let change_reason = + format!("Context deleted by {}", email.clone()); + all_descriptions.push(description.clone()); + all_change_reasons.push(change_reason.clone()); match deleted_row { // Any kind of error would rollback the tranction but explicitly returning rollback tranction allows you to rollback from any point in transaction. Ok(0) => { @@ -799,6 +991,7 @@ async fn bulk_operations( true, &user, &tenant_config, + &tenant, ) .map_err(|err| { log::error!( @@ -807,12 +1000,26 @@ async fn bulk_operations( ); err })?; + all_descriptions.push(move_context_resp.description.clone()); + all_change_reasons.push(move_context_resp.change_reason.clone()); + response.push(ContextBulkResponse::Move(move_context_resp)); } } } - let version_id = add_config_version(&state, tags, transaction_conn)?; + let combined_description = all_descriptions.join(","); + + let combined_change_reasons = all_change_reasons.join(","); + + let version_id = add_config_version( + &state, + tags, + combined_description, + combined_change_reasons, + transaction_conn, + &tenant, + )?; Ok((response, version_id)) })?; let mut http_resp = HttpResponse::Ok(); @@ -832,23 +1039,30 @@ async fn weight_recompute( state: Data, custom_headers: CustomHeaders, db_conn: DbConnection, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, user: User, ) -> superposition::Result { - use superposition_types::cac::schema::contexts::dsl::*; + use superposition_types::database::schema::contexts::dsl::{ + contexts, last_modified_at, last_modified_by, weight, + }; let DbConnection(mut conn) = db_conn; - let result: Vec = contexts.load(&mut conn).map_err(|err| { - log::error!("failed to fetch contexts with error: {}", err); - unexpected_error!("Something went wrong") - })?; + let result: Vec = + contexts + .schema_name(&tenant) + .load(&mut conn) + .map_err(|err| { + log::error!("failed to fetch contexts with error: {}", err); + unexpected_error!("Something went wrong") + })?; - let dimension_data = get_dimension_data(&mut conn)?; + let dimension_data = get_dimension_data(&mut conn, &tenant)?; let dimension_data_map = get_dimension_data_map(&dimension_data)?; let mut response: Vec = vec![]; let tags = parse_config_tags(custom_headers.config_tags)?; - let contexts_new_weight: Vec<(BigDecimal, String)> = result + // Recompute weights and add descriptions + let contexts_new_weight: Vec<(BigDecimal, String, String, String)> = result .clone() .into_iter() .map(|context| { @@ -864,8 +1078,15 @@ async fn weight_recompute( condition: context.value.clone(), old_weight: context.weight.clone(), new_weight: val.clone(), + description: context.description.clone(), + change_reason: context.change_reason.clone(), }); - Ok((val, context.id.clone())) + Ok(( + val, + context.id.clone(), + context.description.clone(), + context.change_reason.clone(), + )) } Err(e) => { log::error!("failed to calculate context weight: {}", e); @@ -873,14 +1094,20 @@ async fn weight_recompute( } } }) - .collect::>>()?; + .collect::>>()?; + // Update database and add config version let last_modified_time = Utc::now().naive_utc(); let config_version_id = conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { - for (context_weight, context_id) in contexts_new_weight { + for (context_weight, context_id, _description, _change_reason) in contexts_new_weight.clone() { diesel::update(contexts.filter(id.eq(context_id))) - .set((weight.eq(context_weight), last_modified_at.eq(last_modified_time.clone()), last_modified_by.eq(user.get_email()))) + .set(( + weight.eq(context_weight), + last_modified_at.eq(last_modified_time.clone()), + last_modified_by.eq(user.get_email()) + )) + .schema_name(&tenant) .execute(transaction_conn).map_err(|err| { log::error!( "Failed to execute query while recomputing weight, error: {err}" @@ -888,11 +1115,14 @@ async fn weight_recompute( db_error!(err) })?; } - let version_id = add_config_version(&state, tags, transaction_conn)?; + let description = "Recomputed weight".to_string(); + let change_reason = "Recomputed weight".to_string(); + let version_id = add_config_version(&state, tags, description, change_reason, transaction_conn, &tenant)?; Ok(version_id) })?; #[cfg(feature = "high-performance-mode")] put_config_in_redis(config_version_id, state, tenant, &mut conn).await?; + let mut http_resp = HttpResponse::Ok(); http_resp.insert_header(( AppHeader::XConfigVersion.to_string(), diff --git a/crates/context_aware_config/src/api/context/helpers.rs b/crates/context_aware_config/src/api/context/helpers.rs index 70a8139a..53992355 100644 --- a/crates/context_aware_config/src/api/context/helpers.rs +++ b/crates/context_aware_config/src/api/context/helpers.rs @@ -3,27 +3,42 @@ extern crate base64; use std::collections::HashMap; use std::str; +use actix_web::web::Json; use base64::prelude::*; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; +use cac_client::utils::json_to_sorted_string; +use chrono::Utc; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; use serde_json::{Map, Value}; -use service_utils::helpers::extract_dimensions; +use service_utils::{helpers::extract_dimensions, service::types::Tenant}; use superposition_macros::{unexpected_error, validation_error}; use superposition_types::{ - cac::schema::{ - default_configs::dsl, - dimensions::{self}, + database::{ + models::cac::Context, + schema::{contexts, default_configs::dsl, dimensions}, }, - result as superposition, Condition, + result as superposition, Cac, Condition, DBConnection, Overrides, TenantConfig, User, }; -use crate::api::context::types::FunctionsInfo; use crate::api::functions::helpers::get_published_functions_by_names; use crate::validation_functions::execute_fn; +use crate::{ + api::{ + context::types::FunctionsInfo, + dimension::{get_dimension_data, get_dimension_data_map}, + }, + helpers::calculate_context_weight, +}; -type DBConnection = PooledConnection>; +use super::{ + types::PutResp, + validations::{validate_dimensions, validate_override_with_default_configs}, + PutReq, +}; + +pub fn hash(val: &Value) -> String { + let sorted_str: String = json_to_sorted_string(val); + blake3::hash(sorted_str.as_bytes()).to_string() +} pub fn validate_condition_with_mandatory_dimensions( context: &Condition, @@ -46,6 +61,7 @@ pub fn validate_condition_with_mandatory_dimensions( pub fn validate_condition_with_functions( conn: &mut DBConnection, context: &Condition, + tenant: &Tenant, ) -> superposition::Result<()> { use dimensions::dsl; let context = extract_dimensions(context)?; @@ -53,13 +69,15 @@ pub fn validate_condition_with_functions( let keys_function_array: Vec<(String, Option)> = dsl::dimensions .filter(dsl::dimension.eq_any(dimensions_list)) .select((dsl::dimension, dsl::function_name)) + .schema_name(tenant) .load(conn)?; let new_keys_function_array: Vec<(String, String)> = keys_function_array .into_iter() .filter_map(|(key_, f_name)| f_name.map(|func| (key_, func))) .collect(); - let dimension_functions_map = get_functions_map(conn, new_keys_function_array)?; + let dimension_functions_map = + get_functions_map(conn, new_keys_function_array, tenant)?; for (key, value) in context.iter() { if let Some(functions_map) = dimension_functions_map.get(key) { if let (function_name, Some(function_code)) = @@ -75,6 +93,7 @@ pub fn validate_condition_with_functions( pub fn validate_override_with_functions( conn: &mut DBConnection, override_: &Map, + tenant: &Tenant, ) -> superposition::Result<()> { let default_config_keys: Vec = override_.keys().cloned().collect(); let keys_function_array: Vec<(String, Option)> = dsl::default_configs @@ -86,7 +105,8 @@ pub fn validate_override_with_functions( .filter_map(|(key_, f_name)| f_name.map(|func| (key_, func))) .collect(); - let default_config_functions_map = get_functions_map(conn, new_keys_function_array)?; + let default_config_functions_map = + get_functions_map(conn, new_keys_function_array, tenant)?; for (key, value) in override_.iter() { if let Some(functions_map) = default_config_functions_map.get(key) { if let (function_name, Some(function_code)) = @@ -102,6 +122,7 @@ pub fn validate_override_with_functions( fn get_functions_map( conn: &mut DBConnection, keys_function_array: Vec<(String, String)>, + tenant: &Tenant, ) -> superposition::Result> { let functions_map: HashMap> = get_published_functions_by_names( @@ -110,6 +131,7 @@ fn get_functions_map( .iter() .map(|(_, f_name)| f_name.clone()) .collect(), + tenant, )? .into_iter() .collect(); @@ -156,3 +178,118 @@ pub fn validate_value_with_function( } Ok(()) } + +pub fn create_ctx_from_put_req( + req: Json, + conn: &mut DBConnection, + user: &User, + tenant_config: &TenantConfig, + tenant: &Tenant, +) -> superposition::Result { + let ctx_condition = req.context.to_owned().into_inner(); + let condition_val = Value::Object(ctx_condition.clone().into()); + let r_override = req.r#override.clone().into_inner(); + let ctx_override = Value::Object(r_override.clone().into()); + validate_condition_with_mandatory_dimensions( + &ctx_condition, + &tenant_config.mandatory_dimensions, + )?; + validate_override_with_default_configs(conn, &r_override, tenant)?; + validate_condition_with_functions(conn, &ctx_condition, tenant)?; + validate_override_with_functions(conn, &r_override, tenant)?; + + let dimension_data = get_dimension_data(conn, tenant)?; + let dimension_data_map = get_dimension_data_map(&dimension_data)?; + validate_dimensions("context", &condition_val, &dimension_data_map)?; + + let weight = calculate_context_weight(&condition_val, &dimension_data_map) + .map_err(|_| unexpected_error!("Something Went Wrong"))?; + + let context_id = hash(&condition_val); + let override_id = hash(&ctx_override); + Ok(Context { + id: context_id, + value: ctx_condition, + override_id, + override_: r_override, + created_at: Utc::now(), + created_by: user.get_email(), + last_modified_at: Utc::now().naive_utc(), + last_modified_by: user.get_email(), + weight, + description: req.description.clone().unwrap_or_default(), + change_reason: req.change_reason.clone(), + }) +} + +fn db_update_override( + conn: &mut DBConnection, + ctx: Context, + user: &User, + tenant: Tenant, +) -> superposition::Result { + use contexts::dsl; + let update_resp = diesel::update(dsl::contexts) + .filter(dsl::id.eq(ctx.id.clone())) + .set(( + dsl::override_.eq(ctx.override_), + dsl::override_id.eq(ctx.override_id), + dsl::last_modified_at.eq(Utc::now().naive_utc()), + dsl::last_modified_by.eq(user.get_email()), + )) + .returning(Context::as_returning()) + .schema_name(&tenant) + .get_result::(conn)?; + Ok(update_resp.into()) +} + +pub fn replace_override_of_existing_ctx( + conn: &mut DBConnection, + ctx: Context, + user: &User, + tenant: Tenant, +) -> superposition::Result { + let new_override = ctx.override_; + let new_override_id = hash(&Value::Object(new_override.clone().into())); + let new_ctx = Context { + override_: new_override, + override_id: new_override_id, + ..ctx + }; + db_update_override(conn, new_ctx, user, tenant) +} + +pub fn update_override_of_existing_ctx( + conn: &mut DBConnection, + ctx: Context, + user: &User, + tenant: Tenant, +) -> superposition::Result { + use contexts::dsl; + let mut new_override: Value = dsl::contexts + .filter(dsl::id.eq(ctx.id.clone())) + .select(dsl::override_) + .schema_name(&tenant) + .first(conn)?; + cac_client::merge( + &mut new_override, + &Value::Object(ctx.override_.clone().into()), + ); + let new_override_id = hash(&new_override); + let new_ctx = Context { + override_: Cac::::validate_db_data( + new_override.as_object().cloned().unwrap_or(Map::new()), + ) + .map_err(|err| { + log::error!( + "update_override_of_existing_ctx : failed to decode context from db {}", + err + ); + unexpected_error!(err) + })? + .into_inner(), + override_id: new_override_id, + ..ctx + }; + db_update_override(conn, new_ctx, user, tenant) +} diff --git a/crates/context_aware_config/src/api/context/operations.rs b/crates/context_aware_config/src/api/context/operations.rs new file mode 100644 index 00000000..a6b6f0a7 --- /dev/null +++ b/crates/context_aware_config/src/api/context/operations.rs @@ -0,0 +1,200 @@ +use actix_web::web::Json; +use chrono::Utc; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + result::{DatabaseErrorKind::*, Error::DatabaseError}, + Connection, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, SelectableHelper, +}; +use serde_json::Value; +use service_utils::service::types::Tenant; +use superposition_macros::{db_error, not_found, unexpected_error}; +use superposition_types::{ + database::{models::cac::Context, schema::contexts}, + result, DBConnection, TenantConfig, User, +}; + +use crate::{ + api::{ + context::{ + helpers::{ + create_ctx_from_put_req, hash, replace_override_of_existing_ctx, + update_override_of_existing_ctx, + validate_condition_with_mandatory_dimensions, + }, + validations::validate_dimensions, + }, + dimension::{get_dimension_data, get_dimension_data_map}, + }, + helpers::calculate_context_weight, +}; + +use super::{ + types::{MoveReq, PutResp}, + PutReq, +}; + +pub fn put( + req: Json, + conn: &mut PooledConnection>, + already_under_txn: bool, + user: &User, + tenant: Tenant, + tenant_config: &TenantConfig, + replace: bool, +) -> result::Result { + use contexts::dsl::contexts; + let new_ctx = create_ctx_from_put_req(req, conn, user, tenant_config, &tenant)?; + + if already_under_txn { + diesel::sql_query("SAVEPOINT put_ctx_savepoint").execute(conn)?; + } + let insert = diesel::insert_into(contexts) + .values(&new_ctx) + .returning(Context::as_returning()) + .schema_name(&tenant) + .execute(conn); + + match insert { + Ok(_) => Ok(new_ctx.into()), + Err(DatabaseError(UniqueViolation, _)) => { + if already_under_txn { + diesel::sql_query("ROLLBACK TO put_ctx_savepoint").execute(conn)?; + } + if replace { + replace_override_of_existing_ctx(conn, new_ctx, user, tenant) + } else { + update_override_of_existing_ctx(conn, new_ctx, user, tenant) + } + } + Err(e) => { + log::error!("failed to update context with db error: {:?}", e); + Err(db_error!(e)) + } + } +} + +pub fn r#move( + old_ctx_id: String, + req: Json, + conn: &mut PooledConnection>, + already_under_txn: bool, + user: &User, + tenant: Tenant, + tenant_config: &TenantConfig, +) -> result::Result { + use contexts::dsl; + let req = req.into_inner(); + let ctx_condition = req.context.to_owned().into_inner(); + let ctx_condition_value = Value::Object(ctx_condition.clone().into()); + let new_ctx_id = hash(&ctx_condition_value); + + let dimension_data = get_dimension_data(conn, &tenant)?; + let dimension_data_map = get_dimension_data_map(&dimension_data)?; + validate_dimensions("context", &ctx_condition_value, &dimension_data_map)?; + let weight = calculate_context_weight(&ctx_condition_value, &dimension_data_map) + .map_err(|_| unexpected_error!("Something Went Wrong"))?; + + validate_condition_with_mandatory_dimensions( + &req.context.into_inner(), + &tenant_config.mandatory_dimensions, + )?; + + if already_under_txn { + diesel::sql_query("SAVEPOINT update_ctx_savepoint").execute(conn)?; + } + + let context = diesel::update(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .set(( + dsl::id.eq(&new_ctx_id), + dsl::value.eq(&ctx_condition_value), + dsl::weight.eq(&weight), + dsl::last_modified_at.eq(Utc::now().naive_utc()), + dsl::last_modified_by.eq(user.get_email()), + )) + .returning(Context::as_returning()) + .schema_name(&tenant) + .get_result::(conn); + + let contruct_new_ctx_with_old_overrides = |ctx: Context| Context { + id: new_ctx_id, + value: ctx_condition, + created_at: Utc::now(), + created_by: user.get_email(), + override_id: ctx.override_id, + override_: ctx.override_, + last_modified_at: Utc::now().naive_utc(), + last_modified_by: user.get_email(), + weight, + description: ctx.description, + change_reason: ctx.change_reason, + }; + + let handle_unique_violation = + |db_conn: &mut DBConnection, already_under_txn: bool| { + if already_under_txn { + let deleted_ctxt = diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .schema_name(&tenant) + .get_result(db_conn)?; + + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(db_conn, ctx, user, tenant) + } else { + db_conn.transaction(|conn| { + let deleted_ctxt = diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .schema_name(&tenant) + .get_result(conn)?; + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(conn, ctx, user, tenant) + }) + } + }; + + match context { + Ok(ctx) => Ok(ctx.into()), + Err(DatabaseError(UniqueViolation, _)) => { + if already_under_txn { + diesel::sql_query("ROLLBACK TO update_ctx_savepoint").execute(conn)?; + } + handle_unique_violation(conn, already_under_txn) + } + Err(e) => { + log::error!("failed to move context with db error: {:?}", e); + Err(db_error!(e)) + } + } +} + +pub fn delete( + ctx_id: String, + user: User, + conn: &mut DBConnection, + tenant: Tenant, +) -> result::Result<()> { + use contexts::dsl; + diesel::update(dsl::contexts) + .filter(dsl::id.eq(&ctx_id)) + .set(( + dsl::last_modified_at.eq(Utc::now().naive_utc()), + dsl::last_modified_by.eq(user.get_email()), + )) + .returning(Context::as_returning()) + .schema_name(&tenant) + .execute(conn)?; + let deleted_row = diesel::delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))) + .schema_name(&tenant) + .execute(conn); + match deleted_row { + Ok(0) => Err(not_found!("Context Id `{}` doesn't exists", ctx_id)), + Ok(_) => { + log::info!("{ctx_id} context deleted by {}", user.get_email()); + Ok(()) + } + Err(e) => { + log::error!("context delete query failed with error: {e}"); + Err(unexpected_error!("Something went wrong.")) + } + } +} diff --git a/crates/context_aware_config/src/api/context/types.rs b/crates/context_aware_config/src/api/context/types.rs index 10bd728b..673d734f 100644 --- a/crates/context_aware_config/src/api/context/types.rs +++ b/crates/context_aware_config/src/api/context/types.rs @@ -1,7 +1,8 @@ use bigdecimal::BigDecimal; use serde::{Deserialize, Serialize}; use superposition_types::{ - custom_query::CommaSeparatedStringQParams, Cac, Condition, Overrides, SortBy, + custom_query::CommaSeparatedStringQParams, database::models::cac::Context, Cac, + Condition, Overrides, SortBy, }; #[cfg_attr(test, derive(Debug, PartialEq))] // Derive traits only when running tests @@ -9,12 +10,16 @@ use superposition_types::{ pub struct PutReq { pub context: Cac, pub r#override: Cac, + pub description: Option, + pub change_reason: String, } #[cfg_attr(test, derive(Debug, PartialEq))] // Derive traits only when running tests #[derive(Deserialize, Clone)] pub struct MoveReq { pub context: Cac, + pub description: Option, + pub change_reason: String, } #[derive(Deserialize, Clone)] @@ -27,6 +32,20 @@ pub struct PutResp { pub context_id: String, pub override_id: String, pub weight: BigDecimal, + pub description: String, + pub change_reason: String, +} + +impl From for PutResp { + fn from(value: Context) -> Self { + PutResp { + context_id: value.id, + override_id: value.override_id, + weight: value.weight, + description: value.description, + change_reason: value.change_reason, + } + } } #[derive(Deserialize)] @@ -81,6 +100,8 @@ pub struct WeightRecomputeResponse { pub condition: Condition, pub old_weight: BigDecimal, pub new_weight: BigDecimal, + pub description: String, + pub change_reason: String, } #[cfg(test)] @@ -100,7 +121,9 @@ mod tests { }, "override": { "foo": "baz" - } + }, + "description": "", + "change_reason": "" }); let action_str = json!({ @@ -122,6 +145,8 @@ mod tests { let expected_action = ContextAction::Put(PutReq { context: context, r#override: override_, + description: Some("".to_string()), + change_reason: "".to_string(), }); let action_deserialized = diff --git a/crates/context_aware_config/src/api/context/validations.rs b/crates/context_aware_config/src/api/context/validations.rs new file mode 100644 index 00000000..10233c4e --- /dev/null +++ b/crates/context_aware_config/src/api/context/validations.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; + +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Map, Value}; +use service_utils::helpers::validation_err_to_str; +use service_utils::service::types::Tenant; +use superposition_macros::{bad_argument, validation_error}; +use superposition_types::{database::schema, result, DBConnection}; + +use crate::helpers::{validate_context_jsonschema, DimensionData}; + +use super::types::DimensionCondition; + +pub fn validate_override_with_default_configs( + conn: &mut DBConnection, + override_: &Map, + tenant: &Tenant, +) -> result::Result<()> { + let keys_array: Vec<&String> = override_.keys().collect(); + let res: Vec<(String, Value)> = schema::default_configs::dsl::default_configs + .filter(schema::default_configs::dsl::key.eq_any(keys_array)) + .select(( + schema::default_configs::dsl::key, + schema::default_configs::dsl::schema, + )) + .schema_name(tenant) + .get_results::<(String, Value)>(conn)?; + + let map = Map::from_iter(res); + + for (key, value) in override_.iter() { + let schema = map + .get(key) + .ok_or(bad_argument!("failed to get schema for config key {}", key))?; + let instance = value; + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(schema); + let jschema = match schema_compile_result { + Ok(jschema) => jschema, + Err(e) => { + log::info!("Failed to compile as a Draft-7 JSON schema: {e}"); + return Err(bad_argument!( + "failed to compile ({}) config key schema", + key + )); + } + }; + if let Err(e) = jschema.validate(instance) { + let verrors = e.collect::>(); + log::error!("({key}) config key validation error: {:?}", verrors); + return Err(validation_error!( + "schema validation failed for {key}: {}", + validation_err_to_str(verrors) + .first() + .unwrap_or(&String::new()) + )); + }; + } + + Ok(()) +} + +pub fn validate_dimensions( + object_key: &str, + cond: &Value, + dimension_schema_map: &HashMap, +) -> result::Result<()> { + let check_dimension = |key: &String, val: &Value| -> result::Result<()> { + if key == "var" { + let dimension_name = val + .as_str() + .ok_or(bad_argument!("Dimension name should be of `String` type"))?; + dimension_schema_map + .get(dimension_name) + .map(|_| Ok(())) + .ok_or(bad_argument!( + "No matching dimension ({}) found", + dimension_name + ))? + } else { + validate_dimensions(key, val, dimension_schema_map) + } + }; + + match cond { + Value::Object(x) => x + .iter() + .try_for_each(|(key, val)| check_dimension(key, val)), + Value::Array(arr) => { + let mut val: Option = None; + let mut condition: Option = None; + for i in arr { + if let (None, Ok(x)) = ( + &condition, + serde_json::from_value::(json!(i)), + ) { + condition = Some(x); + } else if val.is_none() { + val = Some(i.clone()); + } + + if let (Some(_dimension_value), Some(_dimension_condition)) = + (&val, &condition) + { + break; + } + } + + if let (Some(dimension_value), Some(dimension_condition)) = (val, condition) { + let expected_dimension_name = dimension_condition.var; + let dimension_data = dimension_schema_map + .get(&expected_dimension_name) + .ok_or(bad_argument!( + "No matching `dimension` {} in dimension table", + expected_dimension_name + ))?; + + validate_context_jsonschema( + object_key, + &dimension_value, + &dimension_data.schema, + )?; + } + arr.iter().try_for_each(|x| { + validate_dimensions(object_key, x, dimension_schema_map) + }) + } + _ => Ok(()), + } +} diff --git a/crates/context_aware_config/src/api/default_config/handlers.rs b/crates/context_aware_config/src/api/default_config/handlers.rs index 4bea3cb1..28b541e9 100644 --- a/crates/context_aware_config/src/api/default_config/handlers.rs +++ b/crates/context_aware_config/src/api/default_config/handlers.rs @@ -1,34 +1,28 @@ extern crate base64; use actix_web::{ - delete, get, put, + delete, get, post, put, web::{self, Data, Json, Path, Query}, HttpResponse, Scope, }; use chrono::Utc; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; -use diesel::{Connection, SelectableHelper}; +use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::Value; -#[cfg(feature = "high-performance-mode")] -use service_utils::service::types::Tenant; use service_utils::{ helpers::{parse_config_tags, validation_err_to_str}, - service::types::{AppHeader, AppState, CustomHeaders, DbConnection}, + service::types::{AppHeader, AppState, CustomHeaders, DbConnection, Tenant}, }; use superposition_macros::{ bad_argument, db_error, not_found, unexpected_error, validation_error, }; use superposition_types::{ - cac::{ - models::{self as models, Context, DefaultConfig}, + custom_query::PaginationParams, + database::{ + models::cac::{self as models, Context, DefaultConfig}, schema::{self, contexts::dsl::contexts, default_configs::dsl}, }, - custom_query::PaginationParams, - result as superposition, PaginatedResponse, User, + result as superposition, DBConnection, PaginatedResponse, User, }; #[cfg(feature = "high-performance-mode")] @@ -42,93 +36,50 @@ use crate::{ helpers::add_config_version, }; -use super::types::CreateReq; +use super::types::{CreateReq, FunctionNameEnum, UpdateReq}; pub fn endpoints() -> Scope { - Scope::new("").service(create).service(get).service(delete) + Scope::new("") + .service(create_default_config) + .service(update_default_config) + .service(get) + .service(delete) } -#[put("/{key}")] -async fn create( +#[post("")] +async fn create_default_config( state: Data, - key: web::Path, custom_headers: CustomHeaders, request: web::Json, db_conn: DbConnection, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, user: User, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); - let key = key.into_inner().into(); + let key = req.key; let tags = parse_config_tags(custom_headers.config_tags)?; + let description = req.description; + let change_reason = req.change_reason; - if req.value.is_none() && req.schema.is_none() && req.function_name.is_none() { - log::error!("No data provided in the request body for {key}"); - return Err(bad_argument!("Please provide data in the request body.")); + if req.schema.is_empty() { + return Err(bad_argument!("Schema cannot be empty.")); } - let func_name = match &req.function_name { - Some(Value::String(s)) => Some(s.clone()), - Some(Value::Null) | None => None, - Some(_) => { - return Err(bad_argument!( - "Expected a string or null as the function name.", - )) - } - }; - - let result = fetch_default_key(&key, &mut conn); - - let (value, schema, function_name, created_at_val, created_by_val) = match result { - Ok(default_config_row) => { - let val = req.value.unwrap_or(default_config_row.value); - let schema = req - .schema - .map_or_else(|| default_config_row.schema, Value::Object); - let f_name = if req.function_name == Some(Value::Null) { - None - } else { - func_name.or(default_config_row.function_name) - }; - ( - val, - schema, - f_name, - default_config_row.created_at, - default_config_row.created_by, - ) - } - Err(superposition::AppError::DbError(diesel::NotFound)) => { - match (req.value, req.schema) { - (Some(val), Some(schema)) => ( - val, - Value::Object(schema), - func_name, - Utc::now(), - user.get_email(), - ), - _ => { - log::error!("No record found for {key}."); - return Err(bad_argument!("No record found for {}", key)); - } - } - } - Err(e) => { - log::error!("Failed to fetch default_config {key} with error: {e}."); - return Err(unexpected_error!("Something went wrong.")); - } - }; + let value = req.value; + let schema = Value::Object(req.schema); let default_config = DefaultConfig { key: key.to_owned(), value, schema, - function_name, - created_by: created_by_val, - created_at: created_at_val, + function_name: req.function_name, + created_by: user.get_email(), + created_at: Utc::now(), last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), + description: description.clone(), + change_reason: change_reason.clone(), }; let schema_compile_result = JSONSchema::options() @@ -156,40 +107,41 @@ async fn create( )); } - if let Some(f_name) = &default_config.function_name { - let function_code = get_published_function_code(&mut conn, f_name.to_string()) - .map_err(|e| { - log::info!("Function not found with error : {e}"); - bad_argument!("Function {} doesn't exists.", f_name) - })?; - if let Some(f_code) = function_code { - validate_value_with_function( - f_name, - &f_code, - &default_config.key, - &default_config.value, - )?; - } + if let Err(e) = validate_and_get_function_code( + &mut conn, + default_config.function_name.as_ref(), + &default_config.key, + &default_config.value, + &tenant, + ) { + log::info!("Validation failed: {:?}", e); + return Err(e); } + let version_id = conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { - let upsert = diesel::insert_into(dsl::default_configs) + diesel::insert_into(dsl::default_configs) .values(&default_config) - .on_conflict(schema::default_configs::key) - .do_update() - .set(&default_config) - .execute(transaction_conn); - let version_id = add_config_version(&state, tags, transaction_conn)?; - match upsert { - Ok(_) => Ok(version_id), - Err(e) => { + .returning(DefaultConfig::as_returning()) + .schema_name(&tenant) + .execute(transaction_conn) + .map_err(|e| { log::info!("DefaultConfig creation failed with error: {e}"); - Err(unexpected_error!( + unexpected_error!( "Something went wrong, failed to create DefaultConfig" - )) - } - } + ) + })?; + let version_id = add_config_version( + &state, + tags, + description, + change_reason, + transaction_conn, + &tenant, + )?; + Ok(version_id) })?; + #[cfg(feature = "high-performance-mode")] put_config_in_redis(version_id, state, tenant, &mut conn).await?; let mut http_resp = HttpResponse::Ok(); @@ -198,16 +150,164 @@ async fn create( AppHeader::XConfigVersion.to_string(), version_id.to_string(), )); + Ok(http_resp.json(default_config)) } +#[put("/{key}")] +async fn update_default_config( + state: web::Data, + key: web::Path, + custom_headers: CustomHeaders, + request: web::Json, + db_conn: DbConnection, + tenant: Tenant, + user: User, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + let key_str = key.into_inner().into(); + let tags = parse_config_tags(custom_headers.config_tags)?; + + let existing = + fetch_default_key(&key_str, &mut conn, &tenant).map_err(|e| match e { + superposition::AppError::DbError(diesel::NotFound) => { + bad_argument!( + "No record found for {}. Use create endpoint instead.", + key_str + ) + } + _ => { + log::error!("Failed to fetch {key_str}: {e}"); + unexpected_error!("Something went wrong.") + } + })?; + + let description = req + .description + .unwrap_or_else(|| existing.description.clone()); + let change_reason = req.change_reason; + + let value = req.value.unwrap_or_else(|| existing.value.clone()); + let schema = req + .schema + .map(Value::Object) + .unwrap_or_else(|| existing.schema.clone()); + let function_name = match req.function_name { + Some(FunctionNameEnum::Name(func_name)) => Some(func_name), + Some(FunctionNameEnum::Remove) => None, + None => existing.function_name.clone(), + }; + let updated_config = DefaultConfig { + key: key_str.to_owned(), + value, + schema, + function_name: function_name.clone(), + created_by: existing.created_by.clone(), + created_at: existing.created_at, + last_modified_at: Utc::now().naive_utc(), + last_modified_by: user.get_email(), + description: description.clone(), + change_reason: change_reason.clone(), + }; + + let jschema = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&updated_config.schema) + .map_err(|e| { + log::info!("Failed to compile JSON schema: {e}"); + bad_argument!("Invalid JSON schema.") + })?; + + if let Err(e) = jschema.validate(&updated_config.value) { + let verrors = e.collect::>(); + log::info!("Validation failed: {:?}", verrors); + return Err(validation_error!( + "Schema validation failed: {}", + validation_err_to_str(verrors) + .get(0) + .unwrap_or(&String::new()) + )); + } + + if let Err(e) = validate_and_get_function_code( + &mut conn, + updated_config.function_name.as_ref(), + &updated_config.key, + &updated_config.value, + &tenant, + ) { + log::info!("Validation failed: {:?}", e); + return Err(e); + } + + let version_id = + conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { + diesel::insert_into(dsl::default_configs) + .values(&updated_config) + .on_conflict(dsl::key) + .do_update() + .set(&updated_config) + .execute(transaction_conn) + .map_err(|e| { + log::info!("Update failed: {e}"); + unexpected_error!("Failed to update DefaultConfig") + })?; + + let version_id = add_config_version( + &state, + tags.clone(), + description, + change_reason, + transaction_conn, + &tenant, + )?; + + Ok(version_id) + })?; + + #[cfg(feature = "high-performance-mode")] + put_config_in_redis(version_id, state, tenant, &mut conn).await?; + + let mut http_resp = HttpResponse::Ok(); + http_resp.insert_header(( + AppHeader::XConfigVersion.to_string(), + version_id.to_string(), + )); + Ok(http_resp.json(updated_config)) +} + +fn validate_and_get_function_code( + conn: &mut DBConnection, + function_name: Option<&String>, + key: &str, + value: &Value, + tenant: &Tenant, +) -> superposition::Result<()> { + if let Some(f_name) = function_name { + let function_code = get_published_function_code(conn, f_name.clone(), &tenant) + .map_err(|_| bad_argument!("Function {} doesn't exist.", f_name))?; + if let Some(f_code) = function_code { + validate_value_with_function( + f_name.as_str(), + &f_code, + &key.to_string(), + value, + )?; + } + } + Ok(()) +} + fn fetch_default_key( key: &String, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { let res = dsl::default_configs .filter(schema::default_configs::key.eq(key)) .select(models::DefaultConfig::as_select()) + .schema_name(tenant) .get_result(conn)?; Ok(res) } @@ -216,11 +316,14 @@ fn fetch_default_key( async fn get( db_conn: DbConnection, filters: Query, + tenant: Tenant, ) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; if let Some(true) = filters.all { - let result: Vec = dsl::default_configs.get_results(&mut conn)?; + let result: Vec = dsl::default_configs + .schema_name(&tenant) + .get_results(&mut conn)?; return Ok(Json(PaginatedResponse { total_pages: 1, total_items: result.len() as i64, @@ -228,12 +331,16 @@ async fn get( })); } - let n_default_configs: i64 = dsl::default_configs.count().get_result(&mut conn)?; + let n_default_configs: i64 = dsl::default_configs + .count() + .schema_name(&tenant) + .get_result(&mut conn)?; let limit = filters.count.unwrap_or(10); let mut builder = dsl::default_configs - .into_boxed() .order(dsl::created_at.desc()) - .limit(limit); + .limit(limit) + .schema_name(&tenant) + .into_boxed(); if let Some(page) = filters.page { let offset = (page - 1) * limit; builder = builder.offset(offset); @@ -249,12 +356,14 @@ async fn get( pub fn get_key_usage_context_ids( key: &str, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result> { - let result: Vec = contexts.load(conn).map_err(|err| { - log::error!("failed to fetch contexts with error: {}", err); - db_error!(err) - })?; + let result: Vec = + contexts.schema_name(tenant).load(conn).map_err(|err| { + log::error!("failed to fetch contexts with error: {}", err); + db_error!(err) + })?; let mut context_ids = vec![]; for context in result.iter() { @@ -272,7 +381,7 @@ async fn delete( path: Path, custom_headers: CustomHeaders, db_conn: DbConnection, - #[cfg(feature = "high-performance-mode")] tenant: Tenant, + tenant: Tenant, user: User, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; @@ -280,8 +389,10 @@ async fn delete( let key: String = path.into_inner().into(); let mut version_id = 0; - fetch_default_key(&key, &mut conn)?; - let context_ids = get_key_usage_context_ids(&key, &mut conn) + + fetch_default_key(&key, &mut conn, &tenant)?; + + let context_ids = get_key_usage_context_ids(&key, &mut conn, &tenant) .map_err(|_| unexpected_error!("Something went wrong"))?; if context_ids.is_empty() { let resp = @@ -292,17 +403,33 @@ async fn delete( dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.get_email()), )) + .returning(DefaultConfig::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)?; + let default_config: DefaultConfig = dsl::default_configs + .filter(dsl::key.eq(&key)) + .first::(transaction_conn)?; + let description = default_config.description; + let change_reason = format!("Context Deleted by {}", user.get_email()); + let deleted_row = diesel::delete(dsl::default_configs.filter(dsl::key.eq(&key))) + .schema_name(&tenant) .execute(transaction_conn); match deleted_row { Ok(0) => { Err(not_found!("default config key `{}` doesn't exists", key)) } Ok(_) => { - version_id = add_config_version(&state, tags, transaction_conn)?; + version_id = add_config_version( + &state, + tags, + description, + change_reason, + transaction_conn, + &tenant, + )?; log::info!( "default config key: {key} deleted by {}", user.get_email() diff --git a/crates/context_aware_config/src/api/default_config/types.rs b/crates/context_aware_config/src/api/default_config/types.rs index 8366d00d..c8a65174 100644 --- a/crates/context_aware_config/src/api/default_config/types.rs +++ b/crates/context_aware_config/src/api/default_config/types.rs @@ -5,11 +5,46 @@ use superposition_types::RegexEnum; #[derive(Debug, Deserialize)] pub struct CreateReq { + pub key: DefaultConfigKey, + pub value: Value, + pub schema: Map, + pub function_name: Option, + pub description: String, + pub change_reason: String, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateReq { #[serde(default, deserialize_with = "deserialize_option")] pub value: Option, pub schema: Option>, - #[serde(default, deserialize_with = "deserialize_option")] - pub function_name: Option, + pub function_name: Option, + pub description: Option, + pub change_reason: String, +} + +#[derive(Debug, Clone)] +pub enum FunctionNameEnum { + Name(String), + Remove, +} + +impl<'de> Deserialize<'de> for FunctionNameEnum { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let map: Value = Deserialize::deserialize(deserializer)?; + match map { + Value::String(func_name) => Ok(Self::Name(func_name)), + Value::Null => Ok(Self::Remove), + _ => { + log::error!("Expected a string or null literal as the function name."); + Err("Expected a string or null literal as the function name.") + .map_err(serde::de::Error::custom) + } + } + } } #[derive(Debug, Deserialize, AsRef, Deref, DerefMut, Into)] diff --git a/crates/context_aware_config/src/api/dimension/handlers.rs b/crates/context_aware_config/src/api/dimension/handlers.rs index 94935328..29705622 100644 --- a/crates/context_aware_config/src/api/dimension/handlers.rs +++ b/crates/context_aware_config/src/api/dimension/handlers.rs @@ -9,14 +9,15 @@ use chrono::Utc; use diesel::{ delete, Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper, }; -use service_utils::service::types::{AppState, DbConnection}; +use service_utils::service::types::{AppState, DbConnection, Tenant}; use superposition_macros::{bad_argument, db_error, not_found, unexpected_error}; use superposition_types::{ - cac::{ - models::Dimension, - schema::{dimensions, dimensions::dsl::*}, - }, custom_query::PaginationParams, + database::{ + models::cac::Dimension, + schema::dimensions::{self, dsl::*}, + types::DimensionWithMandatory, + }, result as superposition, PaginatedResponse, TenantConfig, User, }; @@ -28,7 +29,7 @@ use crate::{ helpers::validate_jsonschema, }; -use super::types::{DeleteReq, DimensionName, DimensionWithMandatory, UpdateReq}; +use super::types::{DeleteReq, DimensionName, UpdateReq}; pub fn endpoints() -> Scope { Scope::new("") @@ -44,6 +45,7 @@ async fn create( req: web::Json, user: User, db_conn: DbConnection, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; @@ -53,6 +55,7 @@ async fn create( let num_rows = dimensions .count() + .schema_name(&tenant) .get_result::(&mut conn) .map_err(|err| { log::error!("failed to fetch number of dimension with error: {}", err); @@ -75,20 +78,26 @@ async fn create( function_name: create_req.function_name.clone(), last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), + description: create_req.description, + change_reason: create_req.change_reason, }; conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { - diesel::update(dimensions) + diesel::update(dimensions::table) .filter(dimensions::position.ge(dimension_data.position)) .set(( last_modified_at.eq(Utc::now().naive_utc()), last_modified_by.eq(user.get_email()), dimensions::position.eq(dimensions::position + 1), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)?; - let insert_resp = diesel::insert_into(dimensions) + let insert_resp = diesel::insert_into(dimensions::table) .values(&dimension_data) - .get_result::(transaction_conn); + .returning(Dimension::as_returning()) + .schema_name(&tenant) + .get_result(transaction_conn); match insert_resp { Ok(inserted_dimension) => { @@ -126,6 +135,7 @@ async fn update( req: web::Json, user: User, db_conn: DbConnection, + tenant: Tenant, tenant_config: TenantConfig, ) -> superposition::Result { let name: String = path.clone().into(); @@ -134,10 +144,12 @@ async fn update( let mut dimension_row: Dimension = dsl::dimensions .filter(dimensions::dimension.eq(name.clone())) + .schema_name(&tenant) .get_result::(&mut conn)?; let num_rows = dimensions .count() + .schema_name(&tenant) .get_result::(&mut conn) .map_err(|err| { log::error!("failed to fetch number of dimension with error: {}", err); @@ -151,6 +163,11 @@ async fn update( dimension_row.schema = schema_value; } + dimension_row.change_reason = update_req.change_reason; + dimension_row.description = update_req + .description + .unwrap_or_else(|| dimension_row.description); + dimension_row.function_name = match update_req.function_name { Some(FunctionNameEnum::Name(func_name)) => Some(func_name), Some(FunctionNameEnum::Remove) => None, @@ -177,6 +194,8 @@ async fn update( dsl::last_modified_by.eq(user.get_email()), dimensions::position.eq((num_rows + 100) as i32), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .get_result::(transaction_conn)?; if previous_position < new_position { @@ -188,6 +207,8 @@ async fn update( dsl::last_modified_by.eq(user.get_email()), dimensions::position.eq(dimensions::position - 1), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)? } else { diesel::update(dsl::dimensions) @@ -198,6 +219,8 @@ async fn update( dsl::last_modified_by.eq(user.get_email()), dimensions::position.eq(dimensions::position + 1), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)? }; } @@ -211,6 +234,8 @@ async fn update( dimensions::schema.eq(dimension_row.schema), dimensions::position.eq(new_position), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .get_result::(transaction_conn) .map_err(|err| db_error!(err)) })?; @@ -227,21 +252,27 @@ async fn get( db_conn: DbConnection, tenant_config: TenantConfig, filters: Query, + tenant: Tenant, ) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; let (total_pages, total_items, result) = match filters.all { Some(true) => { - let result: Vec = dimensions.get_results(&mut conn)?; + let result: Vec = + dimensions.schema_name(&tenant).get_results(&mut conn)?; (1, result.len() as i64, result) } _ => { - let n_dimensions: i64 = dimensions.count().get_result(&mut conn)?; + let n_dimensions: i64 = dimensions + .count() + .schema_name(&tenant) + .get_result(&mut conn)?; let limit = filters.count.unwrap_or(10); let mut builder = dimensions - .into_boxed() + .schema_name(&tenant) .order(created_at.desc()) - .limit(limit); + .limit(limit) + .into_boxed(); if let Some(page) = filters.page { let offset = (page - 1) * limit; builder = builder.offset(offset); @@ -273,14 +304,16 @@ async fn delete_dimension( path: Path, user: User, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result { let name: String = path.into_inner().into(); let DbConnection(mut conn) = db_conn; let dimension_data: Dimension = dimensions::dsl::dimensions .filter(dimensions::dimension.eq(&name)) .select(Dimension::as_select()) + .schema_name(&tenant) .get_result(&mut conn)?; - let context_ids = get_dimension_usage_context_ids(&name, &mut conn) + let context_ids = get_dimension_usage_context_ids(&name, &mut conn, &tenant) .map_err(|_| unexpected_error!("Something went wrong"))?; if context_ids.is_empty() { conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { @@ -291,12 +324,17 @@ async fn delete_dimension( dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.get_email()), )) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)?; diesel::update(dimensions::dsl::dimensions) .filter(dimensions::position.gt(dimension_data.position)) .set(dimensions::position.eq(dimensions::position - 1)) + .returning(Dimension::as_returning()) + .schema_name(&tenant) .execute(transaction_conn)?; let deleted_row = delete(dsl::dimensions.filter(dsl::dimension.eq(&name))) + .schema_name(&tenant) .execute(transaction_conn); match deleted_row { Ok(0) => Err(not_found!("Dimension `{}` doesn't exists", name)), diff --git a/crates/context_aware_config/src/api/dimension/types.rs b/crates/context_aware_config/src/api/dimension/types.rs index 56320928..6eb015d5 100644 --- a/crates/context_aware_config/src/api/dimension/types.rs +++ b/crates/context_aware_config/src/api/dimension/types.rs @@ -1,8 +1,7 @@ -use chrono::{DateTime, NaiveDateTime, Utc}; use derive_more::{AsRef, Deref, DerefMut, Into}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer}; use serde_json::Value; -use superposition_types::{cac::models::Dimension, RegexEnum}; +use superposition_types::RegexEnum; #[derive(Debug, Deserialize)] pub struct CreateReq { @@ -10,6 +9,8 @@ pub struct CreateReq { pub position: Position, pub schema: Value, pub function_name: Option, + pub description: String, + pub change_reason: String, } #[derive(Debug, Deserialize, AsRef, Deref, DerefMut, Into, Clone)] @@ -43,6 +44,8 @@ pub struct UpdateReq { pub position: Option, pub schema: Option, pub function_name: Option, + pub description: Option, + pub change_reason: String, } #[derive(Debug, Clone)] @@ -88,35 +91,6 @@ impl TryFrom for DimensionName { } } -#[derive(Debug, Serialize, Deserialize)] -pub struct DimensionWithMandatory { - pub dimension: String, - pub position: i32, - pub created_at: DateTime, - pub created_by: String, - pub schema: Value, - pub function_name: Option, - pub last_modified_at: NaiveDateTime, - pub last_modified_by: String, - pub mandatory: bool, -} - -impl DimensionWithMandatory { - pub fn new(value: Dimension, mandatory: bool) -> Self { - DimensionWithMandatory { - dimension: value.dimension, - position: value.position, - created_at: value.created_at, - created_by: value.created_by, - schema: value.schema, - function_name: value.function_name, - last_modified_at: value.last_modified_at, - last_modified_by: value.last_modified_by, - mandatory, - } - } -} - #[derive(Debug, Deserialize, AsRef, Deref, DerefMut, Into)] #[serde(try_from = "String")] pub struct DeleteReq(String); diff --git a/crates/context_aware_config/src/api/dimension/utils.rs b/crates/context_aware_config/src/api/dimension/utils.rs index a260f363..6e2838a7 100644 --- a/crates/context_aware_config/src/api/dimension/utils.rs +++ b/crates/context_aware_config/src/api/dimension/utils.rs @@ -1,26 +1,24 @@ use crate::helpers::DimensionData; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - PgConnection, RunQueryDsl, -}; +use diesel::{query_dsl::methods::SchemaNameDsl, RunQueryDsl}; use jsonschema::{Draft, JSONSchema}; -use service_utils::helpers::extract_dimensions; +use service_utils::{helpers::extract_dimensions, service::types::Tenant}; use std::collections::HashMap; use superposition_macros::{bad_argument, db_error, unexpected_error}; use superposition_types::{ - cac::{ - models::{Context, Dimension}, + database::{ + models::cac::{Context, Dimension}, schema::{contexts::dsl::contexts, dimensions::dsl::*}, }, - result as superposition, Cac, Condition, + result as superposition, Cac, Condition, DBConnection, }; use super::types::{DimensionName, Position}; pub fn get_dimension_data( - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result> { - Ok(dimensions.load::(conn)?) + Ok(dimensions.schema_name(tenant).load::(conn)?) } pub fn get_dimension_data_map( @@ -49,12 +47,14 @@ pub fn get_dimension_data_map( pub fn get_dimension_usage_context_ids( key: &str, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result> { - let result: Vec = contexts.load(conn).map_err(|err| { - log::error!("failed to fetch contexts with error: {}", err); - db_error!(err) - })?; + let result: Vec = + contexts.schema_name(tenant).load(conn).map_err(|err| { + log::error!("failed to fetch contexts with error: {}", err); + db_error!(err) + })?; let mut context_ids = vec![]; for context in result.iter() { diff --git a/crates/context_aware_config/src/api/functions/handlers.rs b/crates/context_aware_config/src/api/functions/handlers.rs index e8cffc9c..93b38f8c 100644 --- a/crates/context_aware_config/src/api/functions/handlers.rs +++ b/crates/context_aware_config/src/api/functions/handlers.rs @@ -7,19 +7,19 @@ use actix_web::{ }; use base64::prelude::*; use chrono::Utc; -use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; +use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; use serde_json::json; -use service_utils::service::types::DbConnection; +use service_utils::service::types::{DbConnection, Tenant}; use superposition_macros::{bad_argument, not_found, unexpected_error}; use superposition_types::{ - cac::{ - models::Function, + custom_query::PaginationParams, + database::{ + models::cac::Function, schema::{ self, functions::{dsl, dsl::functions, function_name, last_modified_at}, }, }, - custom_query::PaginationParams, result as superposition, PaginatedResponse, User, }; use validation_functions::{compile_fn, execute_fn}; @@ -48,6 +48,7 @@ async fn create( request: web::Json, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); @@ -64,13 +65,16 @@ async fn create( published_at: None, published_by: None, published_runtime_version: None, - function_description: req.description, + description: req.description, last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), + change_reason: req.change_reason, }; let insert: Result = diesel::insert_into(functions) .values(&function) + .returning(Function::as_returning()) + .schema_name(&tenant) .get_result(&mut conn); match insert { @@ -106,12 +110,13 @@ async fn update( request: web::Json, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; let req = request.into_inner(); let f_name: String = params.into_inner().into(); - let result = match fetch_function(&f_name, &mut conn) { + let result = match fetch_function(&f_name, &mut conn, &tenant) { Ok(val) => val, Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { log::error!("Function not found."); @@ -137,7 +142,7 @@ async fn update( draft_runtime_version: req .runtime_version .unwrap_or(result.draft_runtime_version), - function_description: req.description.unwrap_or(result.function_description), + description: req.description.unwrap_or(result.description), draft_edited_by: user.get_email(), draft_edited_at: Utc::now().naive_utc(), published_code: result.published_code, @@ -146,11 +151,14 @@ async fn update( published_runtime_version: result.published_runtime_version, last_modified_at: Utc::now().naive_utc(), last_modified_by: user.get_email(), + change_reason: req.change_reason, }; let mut updated_function = diesel::update(functions) .filter(schema::functions::function_name.eq(f_name)) .set(new_function) + .returning(Function::as_returning()) + .schema_name(&tenant) .get_result::(&mut conn)?; decode_function(&mut updated_function)?; @@ -161,10 +169,11 @@ async fn update( async fn get( params: web::Path, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; let f_name: String = params.into_inner().into(); - let mut function = fetch_function(&f_name, &mut conn)?; + let mut function = fetch_function(&f_name, &mut conn, &tenant)?; decode_function(&mut function)?; Ok(Json(function)) @@ -174,21 +183,27 @@ async fn get( async fn list_functions( db_conn: DbConnection, filters: Query, + tenant: Tenant, ) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; let (total_pages, total_items, mut data) = match filters.all { Some(true) => { - let result: Vec = functions.get_results(&mut conn)?; + let result: Vec = + functions.schema_name(&tenant).get_results(&mut conn)?; (1, result.len() as i64, result) } _ => { - let n_functions: i64 = functions.count().get_result(&mut conn)?; + let n_functions: i64 = functions + .count() + .schema_name(&tenant) + .get_result(&mut conn)?; let limit = filters.count.unwrap_or(10); let mut builder = functions - .into_boxed() .order(last_modified_at.desc()) - .limit(limit); + .limit(limit) + .schema_name(&tenant) + .into_boxed(); if let Some(page) = filters.page { let offset = (page - 1) * limit; builder = builder.offset(offset); @@ -214,6 +229,7 @@ async fn delete_function( params: web::Path, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let f_name: String = params.into_inner().into(); @@ -224,9 +240,12 @@ async fn delete_function( dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.get_email()), )) + .returning(Function::as_returning()) + .schema_name(&tenant) .execute(&mut conn)?; - let deleted_row = - delete(functions.filter(function_name.eq(&f_name))).execute(&mut conn); + let deleted_row = delete(functions.filter(function_name.eq(&f_name))) + .schema_name(&tenant) + .execute(&mut conn); match deleted_row { Ok(0) => Err(not_found!("Function {} doesn't exists", f_name)), Ok(_) => { @@ -247,12 +266,13 @@ async fn test( params: Path, request: web::Json, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let path_params = params.into_inner(); let fun_name: &String = &path_params.function_name.into(); let req = request.into_inner(); - let mut function = match fetch_function(fun_name, &mut conn) { + let mut function = match fetch_function(fun_name, &mut conn, &tenant) { Ok(val) => val, Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { log::error!("Function not found."); @@ -293,11 +313,12 @@ async fn publish( params: web::Path, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; let fun_name: String = params.into_inner().into(); - let function = match fetch_function(&fun_name, &mut conn) { + let function = match fetch_function(&fun_name, &mut conn, &tenant) { Ok(val) => val, Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { log::error!("Function {} not found.", fun_name); @@ -320,6 +341,8 @@ async fn publish( dsl::published_by.eq(Some(user.get_email())), dsl::published_at.eq(Some(Utc::now().naive_utc())), )) + .returning(Function::as_returning()) + .schema_name(&tenant) .get_result::(&mut conn)?; Ok(Json(updated_function)) diff --git a/crates/context_aware_config/src/api/functions/helpers.rs b/crates/context_aware_config/src/api/functions/helpers.rs index 2bf03617..53d84a43 100644 --- a/crates/context_aware_config/src/api/functions/helpers.rs +++ b/crates/context_aware_config/src/api/functions/helpers.rs @@ -1,26 +1,26 @@ extern crate base64; use base64::prelude::*; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use service_utils::service::types::Tenant; use std::str; use superposition_macros::unexpected_error; use superposition_types::{ - cac::{ - models::Function, + database::{ + models::cac::Function, schema::{self, functions::dsl::functions}, }, - result as superposition, + result as superposition, DBConnection, }; pub fn fetch_function( f_name: &String, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { Ok(functions .filter(schema::functions::function_name.eq(f_name)) + .schema_name(tenant) .get_result::(conn)?) } @@ -50,19 +50,22 @@ pub fn decode_base64_to_string(code: &String) -> superposition::Result { } pub fn get_published_function_code( - conn: &mut PooledConnection>, + conn: &mut DBConnection, f_name: String, + tenant: &Tenant, ) -> superposition::Result> { let function = functions .filter(schema::functions::function_name.eq(f_name)) .select(schema::functions::published_code) + .schema_name(tenant) .first(conn)?; Ok(function) } pub fn get_published_functions_by_names( - conn: &mut PooledConnection>, + conn: &mut DBConnection, function_names: Vec, + tenant: &Tenant, ) -> superposition::Result)>> { let function: Vec<(String, Option)> = functions .filter(schema::functions::function_name.eq_any(function_names)) @@ -70,6 +73,7 @@ pub fn get_published_functions_by_names( schema::functions::function_name, schema::functions::published_code, )) + .schema_name(tenant) .load(conn)?; Ok(function) } diff --git a/crates/context_aware_config/src/api/functions/types.rs b/crates/context_aware_config/src/api/functions/types.rs index c09b7f7c..93d4503b 100644 --- a/crates/context_aware_config/src/api/functions/types.rs +++ b/crates/context_aware_config/src/api/functions/types.rs @@ -8,6 +8,7 @@ pub struct UpdateFunctionRequest { pub function: Option, pub runtime_version: Option, pub description: Option, + pub change_reason: String, } #[derive(Debug, Deserialize)] @@ -16,6 +17,7 @@ pub struct CreateFunctionRequest { pub function: String, pub runtime_version: String, pub description: String, + pub change_reason: String, } #[derive(Debug, Deserialize, AsRef, Deref, DerefMut, Into)] diff --git a/crates/context_aware_config/src/api/type_templates/handlers.rs b/crates/context_aware_config/src/api/type_templates/handlers.rs index 2dbde446..80ced1bb 100644 --- a/crates/context_aware_config/src/api/type_templates/handlers.rs +++ b/crates/context_aware_config/src/api/type_templates/handlers.rs @@ -1,19 +1,23 @@ use actix_web::web::{Json, Path, Query}; use actix_web::{delete, get, post, put, HttpResponse, Scope}; use chrono::Utc; -use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use diesel::{ + ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, +}; use jsonschema::JSONSchema; use serde_json::Value; -use service_utils::service::types::DbConnection; +use service_utils::service::types::{DbConnection, Tenant}; use superposition_macros::{bad_argument, db_error}; -use superposition_types::cac::models::TypeTemplates; use superposition_types::{ - cac::schema::type_templates::{self, dsl}, custom_query::PaginationParams, + database::{ + models::cac::TypeTemplate, + schema::type_templates::{self, dsl}, + }, result as superposition, PaginatedResponse, User, }; -use crate::api::type_templates::types::{TypeTemplateName, TypeTemplateRequest}; +use crate::api::type_templates::types::{TypeTemplateCreateRequest, TypeTemplateName}; pub fn endpoints() -> Scope { Scope::new("") @@ -25,9 +29,10 @@ pub fn endpoints() -> Scope { #[post("")] async fn create_type( - request: Json, + request: Json, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let _ = JSONSchema::compile(&request.type_schema).map_err(|err| { @@ -48,8 +53,12 @@ async fn create_type( type_templates::type_name.eq(type_name), type_templates::created_by.eq(user.email.clone()), type_templates::last_modified_by.eq(user.email.clone()), + type_templates::description.eq(request.description.clone()), + type_templates::change_reason.eq(request.change_reason.clone()), )) - .get_result::(&mut conn) + .returning(TypeTemplate::as_returning()) + .schema_name(&tenant) + .get_result::(&mut conn) .map_err(|err| { log::error!("failed to insert custom type with error: {}", err); db_error!(err) @@ -63,6 +72,7 @@ async fn update_type( path: Path, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let _ = JSONSchema::compile(&request).map_err(|err| { @@ -76,7 +86,30 @@ async fn update_type( err.to_string() ) })?; + + let description = request.get("description").cloned(); let type_name: String = path.into_inner().into(); + let final_description = if description.is_none() { + let existing_template = type_templates::table + .filter(type_templates::type_name.eq(&type_name)) + .first::(&mut conn) + .optional() + .map_err(|err| { + log::error!("Failed to fetch existing type template: {}", err); + db_error!(err) + })?; + + match existing_template { + Some(template) => template.description.clone(), // Use existing description + None => { + return Err(bad_argument!( + "Description is required as the type template does not exist." + )); + } + } + } else { + description.unwrap().to_string() + }; let timestamp = Utc::now().naive_utc(); let updated_type = diesel::update(type_templates::table) @@ -85,8 +118,11 @@ async fn update_type( type_templates::type_schema.eq(request.clone()), type_templates::last_modified_at.eq(timestamp), type_templates::last_modified_by.eq(user.email), + type_templates::description.eq(final_description), )) - .get_result::(&mut conn) + .returning(TypeTemplate::as_returning()) + .schema_name(&tenant) + .get_result::(&mut conn) .map_err(|err| { log::error!("failed to insert custom type with error: {}", err); db_error!(err) @@ -99,6 +135,7 @@ async fn delete_type( path: Path, db_conn: DbConnection, user: User, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let type_name: String = path.into_inner().into(); @@ -108,10 +145,13 @@ async fn delete_type( dsl::last_modified_at.eq(Utc::now().naive_utc()), dsl::last_modified_by.eq(user.email), )) + .returning(TypeTemplate::as_returning()) + .schema_name(&tenant) .execute(&mut conn)?; let deleted_type = diesel::delete(dsl::type_templates.filter(dsl::type_name.eq(type_name))) - .get_result::(&mut conn)?; + .schema_name(&tenant) + .get_result::(&mut conn)?; Ok(HttpResponse::Ok().json(deleted_type)) } @@ -119,12 +159,14 @@ async fn delete_type( async fn list_types( db_conn: DbConnection, filters: Query, -) -> superposition::Result>> { + tenant: Tenant, +) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; if let Some(true) = filters.all { - let result: Vec = - type_templates::dsl::type_templates.get_results(&mut conn)?; + let result: Vec = type_templates::dsl::type_templates + .schema_name(&tenant) + .get_results(&mut conn)?; return Ok(Json(PaginatedResponse { total_pages: 1, total_items: result.len() as i64, @@ -134,17 +176,19 @@ async fn list_types( let n_types: i64 = type_templates::dsl::type_templates .count() + .schema_name(&tenant) .get_result(&mut conn)?; let limit = filters.count.unwrap_or(10); let mut builder = type_templates::dsl::type_templates - .into_boxed() + .schema_name(&tenant) .order(type_templates::dsl::created_at.desc()) - .limit(limit); + .limit(limit) + .into_boxed(); if let Some(page) = filters.page { let offset = (page - 1) * limit; builder = builder.offset(offset); } - let custom_types: Vec = builder.load(&mut conn)?; + let custom_types: Vec = builder.load(&mut conn)?; let total_pages = (n_types as f64 / limit as f64).ceil() as i64; Ok(Json(PaginatedResponse { total_pages, diff --git a/crates/context_aware_config/src/api/type_templates/types.rs b/crates/context_aware_config/src/api/type_templates/types.rs index d7cbbd1f..1b9c90be 100644 --- a/crates/context_aware_config/src/api/type_templates/types.rs +++ b/crates/context_aware_config/src/api/type_templates/types.rs @@ -4,9 +4,11 @@ use serde_json::Value; use superposition_types::RegexEnum; #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct TypeTemplateRequest { +pub struct TypeTemplateCreateRequest { pub type_schema: Value, pub type_name: TypeTemplateName, + pub description: String, + pub change_reason: String, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -16,6 +18,8 @@ pub struct TypeTemplateResponse { pub created_at: String, pub last_modified: String, pub created_by: String, + pub description: String, + pub change_reason: String, } #[derive(Debug, Deserialize, Serialize, AsRef, Deref, DerefMut, Into, Clone)] diff --git a/crates/context_aware_config/src/helpers.rs b/crates/context_aware_config/src/helpers.rs index 6e187f4b..e330abc8 100644 --- a/crates/context_aware_config/src/helpers.rs +++ b/crates/context_aware_config/src/helpers.rs @@ -5,10 +5,7 @@ use actix_web::web::Data; #[cfg(feature = "high-performance-mode")] use chrono::DateTime; use chrono::Utc; -use diesel::{ - r2d2::{ConnectionManager, PooledConnection}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, -}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; #[cfg(feature = "high-performance-mode")] use fred::interfaces::KeysInterface; @@ -17,7 +14,6 @@ use jsonlogic; use jsonschema::{Draft, JSONSchema, ValidationError}; use num_bigint::BigUint; use serde_json::{json, Map, Value}; -#[cfg(feature = "high-performance-mode")] use service_utils::service::types::Tenant; use service_utils::{ helpers::{generate_snowflake_id, validation_err_to_str}, @@ -25,10 +21,11 @@ use service_utils::{ }; use superposition_macros::{bad_argument, db_error, unexpected_error, validation_error}; #[cfg(feature = "high-performance-mode")] -use superposition_types::cac::schema::event_log::dsl as event_log; +use superposition_types::database::schema::event_log::dsl as event_log; +use superposition_types::DBConnection; use superposition_types::{ - cac::{ - models::ConfigVersion, + database::{ + models::cac::ConfigVersion, schema::{ config_versions, contexts::dsl::{self as ctxt}, @@ -215,11 +212,13 @@ pub fn calculate_context_weight( Ok(weight) } pub fn generate_cac( - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { let contexts_vec: Vec<(String, Condition, String, Overrides)> = ctxt::contexts .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) .order_by((ctxt::weight.asc(), ctxt::created_at.asc())) + .schema_name(tenant) .load::<(String, Condition, String, Overrides)>(conn) .map_err(|err| { log::error!("failed to fetch contexts with error: {}", err); @@ -269,6 +268,7 @@ pub fn generate_cac( let default_config_vec = def_conf::default_configs .select((def_conf::key, def_conf::value)) + .schema_name(&tenant) .load::<(String, Value)>(conn) .map_err(|err| { log::error!("failed to fetch default_configs with error: {}", err); @@ -293,11 +293,14 @@ pub fn generate_cac( pub fn add_config_version( state: &Data, tags: Option>, - db_conn: &mut PooledConnection>, + description: String, + change_reason: String, + db_conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { use config_versions::dsl::config_versions; let version_id = generate_snowflake_id(state)?; - let config = generate_cac(db_conn)?; + let config = generate_cac(db_conn, tenant)?; let json_config = json!(config); let config_hash = blake3::hash(json_config.to_string().as_bytes()).to_string(); let config_version = ConfigVersion { @@ -306,9 +309,13 @@ pub fn add_config_version( config_hash, tags, created_at: Utc::now().naive_utc(), + description, + change_reason, }; diesel::insert_into(config_versions) .values(&config_version) + .returning(ConfigVersion::as_returning()) + .schema_name(tenant) .execute(db_conn)?; Ok(version_id) } @@ -318,9 +325,9 @@ pub async fn put_config_in_redis( version_id: i64, state: Data, tenant: Tenant, - db_conn: &mut PooledConnection>, + db_conn: &mut DBConnection, ) -> superposition::Result<()> { - let raw_config = generate_cac(db_conn)?; + let raw_config = generate_cac(db_conn, &tenant)?; let parsed_config = serde_json::to_string(&json!(raw_config)).map_err(|e| { log::error!("failed to convert cac config to string: {}", e); unexpected_error!("could not convert cac config to string") diff --git a/crates/experimentation_client/Cargo.toml b/crates/experimentation_client/Cargo.toml index 1a6ed6ef..e36cad42 100644 --- a/crates/experimentation_client/Cargo.toml +++ b/crates/experimentation_client/Cargo.toml @@ -11,7 +11,9 @@ once_cell = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -superposition_types = { path = "../superposition_types" } +superposition_types = { path = "../superposition_types", features = [ + "experimentation", +] } tokio = { version = "1.29.1", features = ["full"] } [lib] diff --git a/crates/experimentation_client/src/lib.rs b/crates/experimentation_client/src/lib.rs index 1a771925..ec857e02 100644 --- a/crates/experimentation_client/src/lib.rs +++ b/crates/experimentation_client/src/lib.rs @@ -10,13 +10,16 @@ use std::{ use chrono::{DateTime, TimeZone, Utc}; use derive_more::{Deref, DerefMut}; use serde_json::Value; -use superposition_types::Overridden; +use superposition_types::{ + database::models::experimentation::{ExperimentStatusType, VariantType}, + Overridden, PaginatedResponse, +}; use tokio::{ sync::RwLock, time::{self, Duration}, }; pub use types::{Config, Experiment, Experiments, Variants}; -use types::{ExperimentStore, ListExperimentsResponse, Variant, VariantType}; +use types::{ExperimentStore, Variant}; use utils::MapError; #[derive(Clone, Debug)] @@ -32,7 +35,7 @@ pub struct Client { impl Client { pub fn new(config: Config) -> Self { - Client { + Self { client_config: Arc::new(config), experiments: Arc::new(RwLock::new(HashMap::new())), http_client: reqwest::Client::new(), @@ -64,9 +67,7 @@ impl Client { let mut exp_store = self.experiments.write().await; for (exp_id, experiment) in experiments.into_iter() { match experiment.status { - types::ExperimentStatusType::Concluded => { - exp_store.remove(&exp_id) - } + ExperimentStatusType::CONCLUDED => exp_store.remove(&exp_id), _ => exp_store.insert(exp_id, experiment), }; } @@ -213,7 +214,7 @@ impl Client { ) -> Result, String> { if toss < 0 { for variant in applicable_variants.iter() { - if variant.variant_type == VariantType::Experimental { + if variant.variant_type == VariantType::EXPERIMENTAL { return Ok(Some(variant.clone())); } } @@ -255,7 +256,7 @@ async fn get_experiments( .send() .await .map_err_to_string()? - .json::() + .json::>() .await .map_err_to_string()?; diff --git a/crates/experimentation_client/src/types.rs b/crates/experimentation_client/src/types.rs index 433dc4c0..43feb5f5 100644 --- a/crates/experimentation_client/src/types.rs +++ b/crates/experimentation_client/src/types.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::Value; -use superposition_types::{Exp, Overridden, Overrides}; +use superposition_types::{ + database::models::experimentation::{ExperimentStatusType, VariantType}, + Exp, Overridden, Overrides, +}; #[derive(Clone, Debug)] pub struct Config { @@ -11,21 +14,6 @@ pub struct Config { pub poll_frequency: u64, } -#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "UPPERCASE")] -pub(crate) enum ExperimentStatusType { - Created, - InProgress, - Concluded, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] -#[serde(rename_all = "UPPERCASE")] -pub(crate) enum VariantType { - Control, - Experimental, -} - #[repr(C)] #[derive(Deserialize, Serialize, Clone, Debug)] pub struct Variant { @@ -56,10 +44,3 @@ pub struct Experiment { pub type Experiments = Vec; pub(crate) type ExperimentStore = HashMap; - -#[derive(Serialize, Deserialize, Default)] -pub(crate) struct ListExperimentsResponse { - pub(crate) total_items: i64, - pub(crate) total_pages: i64, - pub(crate) data: Experiments, -} diff --git a/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f52609..00000000 --- a/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1..00000000 --- a/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql deleted file mode 100644 index fae36ce9..00000000 --- a/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql +++ /dev/null @@ -1,184 +0,0 @@ --- Your SQL goes here -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; --- --- Name: public; Type: SCHEMA; Schema: -; Owner: - --- -CREATE SCHEMA IF NOT EXISTS public; --- --- Name: experiment_status_type; Type: TYPE; Schema: public; Owner: - --- -CREATE TYPE public.experiment_status_type AS ENUM ( - 'CREATED', - 'CONCLUDED', - 'INPROGRESS' -); --- --- Name: not_null_text; Type: DOMAIN; Schema: public; Owner: - --- -CREATE DOMAIN public.not_null_text AS text NOT NULL; --- --- Name: event_logger(); Type: FUNCTION; Schema: public; Owner: - --- -CREATE OR REPLACE FUNCTION public.event_logger() RETURNS trigger - LANGUAGE plpgsql - AS $$ -DECLARE - old_data json; - new_data json; -BEGIN - IF (TG_OP = 'UPDATE') THEN - old_data := row_to_json(OLD); - new_data := row_to_json(NEW); - INSERT INTO public.event_log - (table_name, user_name, action, original_data, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - new_data, - current_query() - ); - ELSIF (TG_OP = 'DELETE') THEN - old_data := row_to_json(OLD); - INSERT INTO public.event_log - (table_name, user_name, action, original_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - old_data, - current_query() - ); - ELSIF (TG_OP = 'INSERT') THEN - new_data = row_to_json(NEW); - INSERT INTO public.event_log - (table_name, user_name, action, new_data, query) - VALUES ( - TG_TABLE_NAME::TEXT, - session_user::TEXT, - TG_OP, - new_data, - current_query() - ); - END IF; - RETURN NULL; -END; -$$; -SET default_tablespace = ''; -SET default_table_access_method = heap; --- --- Name: experiments; Type: TABLE; Schema: public; Owner: - --- -CREATE TABLE public.experiments ( - id bigint PRIMARY KEY, - created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - created_by text NOT NULL, - last_modified timestamp with time zone DEFAULT now() NOT NULL, - name text NOT NULL, - override_keys public.not_null_text[] NOT NULL, - status public.experiment_status_type NOT NULL, - traffic_percentage integer NOT NULL, - context json NOT NULL, - variants json NOT NULL, - last_modified_by text DEFAULT 'Null'::text NOT NULL, - chosen_variant text, - CONSTRAINT experiments_traffic_percentage_check CHECK ((traffic_percentage >= 0)) -); --- --- Name: experiment_created_date_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX experiment_created_date_index ON public.experiments USING btree (created_at) INCLUDE (id); --- --- Name: experiment_last_modified_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX experiment_last_modified_index ON public.experiments USING btree (last_modified) INCLUDE (id, created_at); --- --- Name: experiment_status_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX experiment_status_index ON public.experiments USING btree (status) INCLUDE (created_at, last_modified); - --- --- Name: event_log; Type: TABLE; Schema: public; Owner: - --- -CREATE TABLE IF NOT EXISTS public.event_log ( - id uuid DEFAULT uuid_generate_v4() NOT NULL, - table_name text NOT NULL, - user_name text NOT NULL, - "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - action text NOT NULL, - original_data json, - new_data json, - query text NOT NULL, - PRIMARY KEY(id, timestamp) -) -PARTITION BY RANGE ("timestamp"); - --- --- Name: event_log_action_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX IF NOT EXISTS event_log_action_index ON ONLY public.event_log USING btree (action) INCLUDE ("timestamp", table_name); --- --- Name: event_log_table_name_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX IF NOT EXISTS event_log_table_name_index ON ONLY public.event_log USING btree (table_name) INCLUDE (action, "timestamp"); --- --- Name: event_log_timestamp_index; Type: INDEX; Schema: public; Owner: - --- -CREATE INDEX IF NOT EXISTS event_log_timestamp_index ON ONLY public.event_log USING btree ("timestamp") INCLUDE (action, table_name); - --- --- event_log table partitions --- -CREATE TABLE IF NOT EXISTS public.event_log_y2023m08 PARTITION OF public.event_log FOR -VALUES -FROM ('2023-08-01') TO ('2023-09-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2023m09 PARTITION OF public.event_log FOR -VALUES -FROM ('2023-09-01') TO ('2023-10-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2023m10 PARTITION OF public.event_log FOR -VALUES -FROM ('2023-10-01') TO ('2023-11-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2023m11 PARTITION OF public.event_log FOR -VALUES -FROM ('2023-11-01') TO ('2023-12-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2023m12 PARTITION OF public.event_log FOR -VALUES -FROM ('2023-12-01') TO ('2024-01-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m01 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-01-01') TO ('2024-02-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m02 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-02-01') TO ('2024-03-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m03 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-03-01') TO ('2024-04-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m04 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-04-01') TO ('2024-05-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m05 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-05-01') TO ('2024-06-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m06 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-06-01') TO ('2024-07-01'); - -CREATE TABLE IF NOT EXISTS public.event_log_y2024m07 PARTITION OF public.event_log FOR -VALUES -FROM ('2024-07-01') TO ('2024-08-01'); - --- --- Name: experiments experiments_audit; Type: TRIGGER; Schema: public; Owner: - --- -CREATE TRIGGER experiments_audit AFTER INSERT OR DELETE OR UPDATE ON public.experiments FOR EACH ROW EXECUTE FUNCTION public.event_logger(); diff --git a/crates/experimentation_platform/src/api/experiments/handlers.rs b/crates/experimentation_platform/src/api/experiments/handlers.rs index bf362448..8235f3db 100644 --- a/crates/experimentation_platform/src/api/experiments/handlers.rs +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -12,7 +12,8 @@ use diesel::{ dsl::sql, r2d2::{ConnectionManager, PooledConnection}, sql_types::{Bool, Text}, - ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, TextExpressionMethods, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, SelectableHelper, + TextExpressionMethods, }; use reqwest::{Method, Response, StatusCode}; use serde_json::{json, Map, Value}; @@ -25,13 +26,16 @@ use service_utils::service::types::{ use superposition_macros::{bad_argument, response_error, unexpected_error}; use superposition_types::{ custom_query::PaginationParams, - experimentation::{ - models::{EventLog, Experiment, ExperimentStatusType, Variant, Variants}, + database::{ + models::experimentation::{ + EventLog, Experiment, ExperimentStatusType, Variant, Variants, + }, schema::{event_log::dsl as event_log, experiments::dsl as experiments}, }, - result::{self as superposition}, + result as superposition, webhook::{WebhookConfig, WebhookEvent}, - Condition, Exp, Overrides, SortBy, TenantConfig, User, + Condition, DBConnection, Exp, Overrides, PaginatedResponse, SortBy, TenantConfig, + User, }; use super::{ @@ -44,7 +48,7 @@ use super::{ ApplicableVariantsQuery, AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, ContextMoveReq, ContextPutReq, ExperimentCreateRequest, ExperimentCreateResponse, ExperimentListFilters, - ExperimentResponse, ExperimentsResponse, OverrideKeysUpdateRequest, RampRequest, + ExperimentResponse, OverrideKeysUpdateRequest, RampRequest, }, }; use crate::api::experiments::{helpers::construct_header_map, types::ExperimentSortOn}; @@ -132,9 +136,11 @@ async fn create( user: User, tenant_config: TenantConfig, ) -> superposition::Result { - use superposition_types::experimentation::schema::experiments::dsl::experiments; + use superposition_types::database::schema::experiments::dsl::experiments; let mut variants = req.variants.to_vec(); let DbConnection(mut conn) = db_conn; + let description = req.description.clone(); + let change_reason = req.change_reason.clone(); // Checking if experiment has exactly 1 control variant, and // atleast 1 experimental variant @@ -178,8 +184,14 @@ async fn create( // validating experiment against other active experiments based on permission flags let flags = &state.experimentation_flags; - let (valid, reason) = - validate_experiment(&exp_context, &unique_override_keys, None, flags, &mut conn)?; + let (valid, reason) = validate_experiment( + &exp_context, + &unique_override_keys, + None, + flags, + &tenant, + &mut conn, + )?; if !valid { return Err(bad_argument!(reason)); } @@ -209,6 +221,8 @@ async fn create( })? .clone(), r#override: json!(variant.overrides), + description: Some(description.clone()), + change_reason: change_reason.clone(), }; cac_operations.push(ContextAction::PUT(payload)); } @@ -278,10 +292,14 @@ async fn create( variants: Variants::new(variants), last_modified_by: user.get_email(), chosen_variant: None, + description, + change_reason, }; let mut inserted_experiments = diesel::insert_into(experiments) .values(&new_experiment) + .returning(Experiment::as_returning()) + .schema_name(&tenant) .get_results(&mut conn)?; let inserted_experiment: Experiment = inserted_experiments.remove(0); @@ -357,14 +375,21 @@ pub async fn conclude( tenant: Tenant, user: User, ) -> superposition::Result<(Experiment, Option)> { - use superposition_types::experimentation::schema::experiments::dsl; + use superposition_types::database::schema::experiments::dsl; + + let change_reason = req.change_reason.clone(); let winner_variant_id: String = req.chosen_variant.to_owned(); let experiment: Experiment = dsl::experiments .find(experiment_id) + .schema_name(&tenant) .get_result::(&mut conn)?; + let description = match req.description.clone() { + Some(desc) => desc, + None => experiment.description.clone(), + }; if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { return Err(bad_argument!( "experiment with id {} is already concluded", @@ -387,6 +412,8 @@ pub async fn conclude( if !experiment_context.is_empty() { let context_move_req = ContextMoveReq { context: experiment_context.clone(), + description: description.clone(), + change_reason: change_reason.clone(), }; operations.push(ContextAction::MOVE((context_id, context_move_req))); } else { @@ -479,6 +506,8 @@ pub async fn conclude( dsl::last_modified_by.eq(user.get_email()), dsl::chosen_variant.eq(Some(winner_variant_id)), )) + .returning(Experiment::as_returning()) + .schema_name(&tenant) .get_result::(&mut conn)?; Ok((updated_experiment, config_version_id)) @@ -488,12 +517,14 @@ pub async fn conclude( async fn get_applicable_variants( db_conn: DbConnection, query_data: Query, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let query_data = query_data.into_inner(); let experiments = experiments::experiments .filter(experiments::status.ne(ExperimentStatusType::CONCLUDED)) + .schema_name(&tenant) .load::(&mut conn)?; let experiments = experiments.into_iter().filter(|exp| { @@ -529,21 +560,27 @@ async fn list_experiments( pagination_params: Query, filters: Query, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result { let DbConnection(mut conn) = db_conn; if let Some(true) = pagination_params.all { - let result = experiments::experiments.get_results::(&mut conn)?; - return Ok(HttpResponse::Ok().json(ExperimentsResponse { - total_pages: 1, - total_items: result.len() as i64, - data: result.into_iter().map(ExperimentResponse::from).collect(), - })); + let result = experiments::experiments + .schema_name(&tenant) + .get_results::(&mut conn)?; + return Ok( + HttpResponse::Ok().json(PaginatedResponse:: { + total_pages: 1, + total_items: result.len() as i64, + data: result.into_iter().map(ExperimentResponse::from).collect(), + }), + ); } let max_event_timestamp: Option = event_log::event_log .filter(event_log::table_name.eq("experiments")) .select(diesel::dsl::max(event_log::timestamp)) + .schema_name(&tenant) .first(&mut conn)?; let last_modified = req @@ -561,7 +598,7 @@ async fn list_experiments( }; let query_builder = |filters: &ExperimentListFilters| { - let mut builder = experiments::experiments.into_boxed(); + let mut builder = experiments::experiments.schema_name(&tenant).into_boxed(); if let Some(ref states) = filters.status { builder = builder.filter(experiments::status.eq_any(states.0.clone())); } @@ -615,33 +652,38 @@ async fn list_experiments( let query = base_query.limit(limit).offset(offset); let experiment_list = query.load::(&mut conn)?; let total_pages = (number_of_experiments as f64 / limit as f64).ceil() as i64; - Ok(HttpResponse::Ok().json(ExperimentsResponse { - total_pages, - total_items: number_of_experiments, - data: experiment_list - .into_iter() - .map(ExperimentResponse::from) - .collect(), - })) + Ok( + HttpResponse::Ok().json(PaginatedResponse:: { + total_pages, + total_items: number_of_experiments, + data: experiment_list + .into_iter() + .map(ExperimentResponse::from) + .collect(), + }), + ) } #[get("/{id}")] async fn get_experiment_handler( params: web::Path, db_conn: DbConnection, + tenant: Tenant, ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; - let response = get_experiment(params.into_inner(), &mut conn)?; + let response = get_experiment(params.into_inner(), &mut conn, &tenant)?; Ok(Json(ExperimentResponse::from(response))) } pub fn get_experiment( experiment_id: i64, - conn: &mut PooledConnection>, + conn: &mut DBConnection, + tenant: &Tenant, ) -> superposition::Result { - use superposition_types::experimentation::schema::experiments::dsl::*; + use superposition_types::database::schema::experiments::dsl::*; let result: Experiment = experiments .find(experiment_id) + .schema_name(&tenant) .get_result::(conn)?; Ok(result) @@ -659,9 +701,12 @@ async fn ramp( ) -> superposition::Result> { let DbConnection(mut conn) = db_conn; let exp_id = params.into_inner(); + let description = req.description.clone(); + let change_reason = req.change_reason.clone(); let experiment: Experiment = experiments::experiments .find(exp_id) + .schema_name(&tenant) .get_result::(&mut conn)?; let old_traffic_percentage = experiment.traffic_percentage as u8; @@ -689,7 +734,11 @@ async fn ramp( experiments::last_modified.eq(Utc::now()), experiments::last_modified_by.eq(user.get_email()), experiments::status.eq(ExperimentStatusType::INPROGRESS), + experiments::description.eq(description), + experiments::change_reason.eq(change_reason), )) + .returning(Experiment::as_returning()) + .schema_name(&tenant) .get_result(&mut conn)?; let (_, config_version_id) = fetch_cac_config(&tenant, &data).await?; @@ -732,6 +781,8 @@ async fn update_overrides( ) -> superposition::Result { let DbConnection(mut conn) = db_conn; let experiment_id = params.into_inner(); + let description = req.description.clone(); + let change_reason = req.change_reason.clone(); let payload = req.into_inner(); let variants = payload.variants; @@ -747,6 +798,7 @@ async fn update_overrides( // fetch the current variants of the experiment let experiment = experiments::experiments .find(experiment_id) + .schema_name(&tenant) .first::(&mut conn)?; if experiment.status != ExperimentStatusType::CREATED { @@ -830,6 +882,7 @@ async fn update_overrides( &override_keys, Some(experiment_id), flags, + &tenant, &mut conn, )?; if !valid { @@ -869,6 +922,8 @@ async fn update_overrides( })? .clone(), r#override: json!(variant.overrides), + description: description.clone(), + change_reason: change_reason.clone(), }; cac_operations.push(ContextAction::PUT(payload)); } @@ -935,6 +990,8 @@ async fn update_overrides( experiments::last_modified.eq(Utc::now()), experiments::last_modified_by.eq(user.get_email()), )) + .returning(Experiment::as_returning()) + .schema_name(&tenant) .get_result::(&mut conn)?; let experiment_response = ExperimentResponse::from(updated_experiment); @@ -962,11 +1019,12 @@ async fn update_overrides( async fn get_audit_logs( filters: Query, db_conn: DbConnection, -) -> superposition::Result { + tenant: Tenant, +) -> superposition::Result>> { let DbConnection(mut conn) = db_conn; let query_builder = |filters: &AuditQueryFilters| { - let mut builder = event_log::event_log.into_boxed(); + let mut builder = event_log::event_log.schema_name(&tenant).into_boxed(); if let Some(tables) = filters.table.clone() { builder = builder.filter(event_log::table_name.eq_any(tables.0)); } @@ -1001,9 +1059,9 @@ async fn get_audit_logs( let total_pages = (log_count as f64 / limit as f64).ceil() as i64; - Ok(HttpResponse::Ok().json(json!({ - "total_items": log_count, - "total_pages": total_pages, - "data": logs - }))) + Ok(Json(PaginatedResponse { + total_items: log_count, + total_pages: total_pages, + data: logs, + })) } diff --git a/crates/experimentation_platform/src/api/experiments/helpers.rs b/crates/experimentation_platform/src/api/experiments/helpers.rs index 0fbd7ccb..580679f0 100644 --- a/crates/experimentation_platform/src/api/experiments/helpers.rs +++ b/crates/experimentation_platform/src/api/experiments/helpers.rs @@ -9,7 +9,9 @@ use std::collections::HashSet; use std::str::FromStr; use superposition_macros::{bad_argument, unexpected_error}; use superposition_types::{ - experimentation::models::{Experiment, ExperimentStatusType, Variant, VariantType}, + database::models::experimentation::{ + Experiment, ExperimentStatusType, Variant, VariantType, + }, result as superposition, Condition, Config, Exp, Overrides, }; @@ -181,9 +183,10 @@ pub fn validate_experiment( override_keys: &[String], experiment_id: Option, flags: &ExperimentationFlags, + tenant: &Tenant, conn: &mut PgConnection, ) -> superposition::Result<(bool, String)> { - use superposition_types::experimentation::schema::experiments::dsl as experiments_dsl; + use superposition_types::database::schema::experiments::dsl as experiments_dsl; let active_experiments: Vec = experiments_dsl::experiments .filter( @@ -194,6 +197,7 @@ pub fn validate_experiment( .or(experiments_dsl::status.eq(ExperimentStatusType::INPROGRESS)), ), ) + .schema_name(tenant) .load(conn)?; is_valid_experiment(context, override_keys, flags, &active_experiments) diff --git a/crates/experimentation_platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs index a5a9108f..c57ff465 100644 --- a/crates/experimentation_platform/src/api/experiments/types.rs +++ b/crates/experimentation_platform/src/api/experiments/types.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use superposition_types::{ custom_query::{deserialize_stringified_list, CommaSeparatedStringQParams}, - experimentation::models::{Experiment, ExperimentStatusType, Variant}, + database::models::experimentation::{Experiment, ExperimentStatusType, Variant}, Condition, Exp, Overrides, SortBy, }; @@ -14,6 +14,8 @@ pub struct ExperimentCreateRequest { pub name: String, pub context: Exp, pub variants: Vec, + pub description: String, + pub change_reason: String, } #[derive(Serialize)] @@ -49,6 +51,8 @@ pub struct ExperimentResponse { pub variants: Vec, pub last_modified_by: String, pub chosen_variant: Option, + pub description: String, + pub change_reason: String, } impl From for ExperimentResponse { @@ -68,22 +72,19 @@ impl From for ExperimentResponse { variants: experiment.variants.into_inner(), last_modified_by: experiment.last_modified_by, chosen_variant: experiment.chosen_variant, + description: experiment.description, + change_reason: experiment.change_reason, } } } -#[derive(Serialize)] -pub struct ExperimentsResponse { - pub total_items: i64, - pub total_pages: i64, - pub data: Vec, -} - /********** Experiment Conclude Req Types **********/ #[derive(Deserialize, Debug)] pub struct ConcludeExperimentRequest { pub chosen_variant: String, + pub description: Option, + pub change_reason: String, } /********** Context Bulk API Type *************/ @@ -92,6 +93,8 @@ pub struct ConcludeExperimentRequest { pub struct ContextPutReq { pub context: Map, pub r#override: Value, + pub description: Option, + pub change_reason: String, } #[derive(Deserialize, Serialize, Clone)] @@ -192,6 +195,8 @@ pub struct ExperimentListFilters { #[derive(Deserialize, Debug)] pub struct RampRequest { pub traffic_percentage: u64, + pub description: String, + pub change_reason: String, } /********** Update API type ********/ @@ -205,11 +210,15 @@ pub struct VariantUpdateRequest { #[derive(Deserialize, Debug)] pub struct OverrideKeysUpdateRequest { pub variants: Vec, + pub description: Option, + pub change_reason: String, } #[derive(Deserialize, Serialize, Clone)] pub struct ContextMoveReq { pub context: Map, + pub description: String, + pub change_reason: String, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/experimentation_platform/tests/experimentation_tests.rs b/crates/experimentation_platform/tests/experimentation_tests.rs index fcbccd55..7dc283ad 100644 --- a/crates/experimentation_platform/tests/experimentation_tests.rs +++ b/crates/experimentation_platform/tests/experimentation_tests.rs @@ -4,13 +4,16 @@ use serde_json::{json, Map, Value}; use service_utils::helpers::extract_dimensions; use service_utils::service::types::ExperimentationFlags; use superposition_types::{ - experimentation::models::{Experiment, ExperimentStatusType, Variant, Variants}, + database::models::experimentation::{ + Experiment, ExperimentStatusType, Variant, Variants, + }, result as superposition, Cac, Condition, Exp, Overrides, }; enum Dimensions { Os(String), Client(String), + #[allow(dead_code)] VariantIds(String), } @@ -70,6 +73,8 @@ fn experiment_gen( context: context.clone(), variants: Variants::new(variants.clone()), chosen_variant: None, + description: "".to_string(), + change_reason: "".to_string(), } } diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index 0918b2a7..4ab90394 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = { workspace = true } -cfg-if = {workspace = true } +cfg-if = { workspace = true } chrono = { workspace = true } console_error_panic_hook = { version = "0.1", optional = true } derive_more = { workspace = true } @@ -24,8 +24,10 @@ serde_json = { workspace = true } reqwest = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -superposition_types = { path = "../superposition_types", default-features = false } -url = "2.5.0" +superposition_types = { path = "../superposition_types", features = [ + "experimentation", +], default-features = false } +url = { workspace = true } wasm-bindgen = "=0.2.89" web-sys = { version = "0.3.64", features = [ "Event", diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index b6e8871d..6fef73e9 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -1,12 +1,18 @@ use leptos::ServerFnError; -use superposition_types::Config; +use superposition_types::{ + custom_query::PaginationParams, + database::{ + models::{ + cac::{ConfigVersion, DefaultConfig, Function, TypeTemplate}, + Workspace, + }, + types::DimensionWithMandatory, + }, + Config, PaginatedResponse, +}; use crate::{ - types::{ - ConfigVersionListResponse, DefaultConfig, Dimension, ExperimentListFilters, - ExperimentResponse, FetchTypeTemplateResponse, FunctionResponse, ListFilters, - PaginatedResponse, - }, + types::{ExperimentListFilters, ExperimentResponse}, utils::{ construct_request_headers, get_host, parse_json_response, request, use_host_server, @@ -15,16 +21,18 @@ use crate::{ // #[server(GetDimensions, "/fxn", "GetJson")] pub async fn fetch_dimensions( - filters: &ListFilters, + filters: &PaginationParams, tenant: String, -) -> Result, ServerFnError> { + org_id: String, +) -> Result, ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); let url = format!("{}/dimension?{}", host, filters.to_string()); - let response: PaginatedResponse = client + let response: PaginatedResponse = client .get(url) .header("x-tenant", &tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? @@ -37,8 +45,9 @@ pub async fn fetch_dimensions( // #[server(GetDefaultConfig, "/fxn", "GetJson")] pub async fn fetch_default_config( - filters: &ListFilters, + filters: &PaginationParams, tenant: String, + org_id: String, ) -> Result, ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); @@ -47,29 +56,29 @@ pub async fn fetch_default_config( let response: PaginatedResponse = client .get(url) .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? .json() .await .map_err(|e| ServerFnError::new(e.to_string()))?; - Ok(response) } pub async fn fetch_snapshots( + filters: &PaginationParams, tenant: String, - page: i64, - count: i64, - all: bool, -) -> Result { + org_id: String, +) -> Result, ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); - let url = format!("{host}/config/versions?page={page}&count={count}&all={all}"); - let response: ConfigVersionListResponse = client + let url = format!("{host}/config/versions?{}", filters.to_string()); + let response: PaginatedResponse = client .get(url) .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? @@ -83,6 +92,7 @@ pub async fn fetch_snapshots( pub async fn delete_context( tenant: String, context_id: String, + org_id: String, ) -> Result<(), ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); @@ -93,6 +103,7 @@ pub async fn delete_context( let response = client .delete(&url) // Make sure to pass the URL by reference here .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))?; @@ -109,8 +120,9 @@ pub async fn delete_context( pub async fn fetch_experiments( filters: &ExperimentListFilters, - pagination: &ListFilters, + pagination: &PaginationParams, tenant: String, + org_id: String, ) -> Result, ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); @@ -129,6 +141,7 @@ pub async fn fetch_experiments( let response: PaginatedResponse = client .get(url) .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? @@ -140,16 +153,18 @@ pub async fn fetch_experiments( } pub async fn fetch_functions( - filters: ListFilters, + filters: &PaginationParams, tenant: String, -) -> Result, ServerFnError> { + org_id: String, +) -> Result, ServerFnError> { let client = reqwest::Client::new(); let host = use_host_server(); let url = format!("{}/function?{}", host, filters.to_string()); - let response: PaginatedResponse = client + let response: PaginatedResponse = client .get(url) .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? @@ -163,14 +178,16 @@ pub async fn fetch_functions( pub async fn fetch_function( function_name: String, tenant: String, -) -> Result { + org_id: String, +) -> Result { let client = reqwest::Client::new(); let host = use_host_server(); let url = format!("{}/function/{}", host, function_name); - let response: FunctionResponse = client + let response: Function = client .get(url) .header("x-tenant", tenant) + .header("x-org-id", org_id) .send() .await .map_err(|e| ServerFnError::new(e.to_string()))? @@ -185,6 +202,7 @@ pub async fn fetch_function( pub async fn fetch_config( tenant: String, version: Option, + org_id: String, ) -> Result { let client = reqwest::Client::new(); let host = use_host_server(); @@ -193,7 +211,13 @@ pub async fn fetch_config( Some(version) => format!("{}/config?version={}", host, version), None => format!("{}/config", host), }; - match client.get(url).header("x-tenant", tenant).send().await { + match client + .get(url) + .header("x-tenant", tenant) + .header("x-org-id", org_id) + .send() + .await + { Ok(response) => { let config: Config = response .json() @@ -209,12 +233,19 @@ pub async fn fetch_config( pub async fn fetch_experiment( exp_id: String, tenant: String, + org_id: String, ) -> Result { let client = reqwest::Client::new(); let host = use_host_server(); let url = format!("{}/experiments/{}", host, exp_id); - match client.get(url).header("x-tenant", tenant).send().await { + match client + .get(url) + .header("x-tenant", tenant) + .header("x-org-id", org_id) + .send() + .await + { Ok(experiment) => { let experiment = experiment .json() @@ -226,7 +257,11 @@ pub async fn fetch_experiment( } } -pub async fn delete_default_config(key: String, tenant: String) -> Result<(), String> { +pub async fn delete_default_config( + key: String, + tenant: String, + org_id: String, +) -> Result<(), String> { let host = get_host(); let url = format!("{host}/default-config/{key}"); @@ -234,14 +269,18 @@ pub async fn delete_default_config(key: String, tenant: String) -> Result<(), St url, reqwest::Method::DELETE, None::, - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; Ok(()) } -pub async fn delete_dimension(name: String, tenant: String) -> Result<(), String> { +pub async fn delete_dimension( + name: String, + tenant: String, + org_id: String, +) -> Result<(), String> { let host = get_host(); let url = format!("{host}/dimension/{name}"); @@ -249,31 +288,67 @@ pub async fn delete_dimension(name: String, tenant: String) -> Result<(), String url, reqwest::Method::DELETE, None::, - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; Ok(()) } +pub async fn fetch_organisations() -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + let url = format!("{host}/organisations"); + + match client.get(url).send().await { + Ok(organisations) => { + let organisations = organisations + .json() + .await + .map_err(|err| ServerFnError::new(err.to_string()))?; + Ok(organisations) + } + Err(e) => Err(ServerFnError::new(e.to_string())), + } +} + pub async fn fetch_types( + filters: &PaginationParams, tenant: String, - page: i64, - count: i64, - all: bool, -) -> Result { + org_id: String, +) -> Result, ServerFnError> { let host = use_host_server(); - let url = format!("{host}/types?page={page}&count={count}&all={all}"); + let url = format!("{host}/types?{}", filters.to_string()); let err_handler = |e: String| ServerFnError::new(e.to_string()); let response = request::<()>( url, reqwest::Method::GET, None, - construct_request_headers(&[("x-tenant", &tenant)]).map_err(err_handler)?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)]) + .map_err(err_handler)?, ) .await .map_err(err_handler)?; - parse_json_response::(response) + parse_json_response::>(response) .await .map_err(err_handler) } + +pub async fn fetch_workspaces( + filters: &PaginationParams, + org_id: &String, +) -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + let url = format!("{}/workspaces?{}", host, filters.to_string()); + let response: PaginatedResponse = client + .get(url) + .header("x-org-id", org_id) + .send() + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(response) +} diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs index aeb26a1c..5d39b154 100755 --- a/crates/frontend/src/app.rs +++ b/crates/frontend/src/app.rs @@ -14,6 +14,7 @@ use crate::pages::function::{ use crate::pages::{ context_override::ContextOverride, custom_types::TypesPage, default_config::DefaultConfig, experiment::ExperimentPage, home::Home, + organisations::Organisations, workspace::Workspace, }; use crate::providers::alert_provider::AlertProvider; use crate::types::Envs; @@ -93,9 +94,20 @@ pub fn app(app_envs: Envs) -> impl IntoView { + + + + } + } + /> @@ -107,7 +119,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -119,7 +131,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -131,7 +143,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -143,7 +155,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -155,7 +167,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -167,7 +179,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -179,7 +191,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -191,7 +203,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -203,7 +215,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -215,7 +227,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -227,7 +239,7 @@ pub fn app(app_envs: Envs) -> impl IntoView { @@ -237,6 +249,18 @@ pub fn app(app_envs: Envs) -> impl IntoView { } /> + + + + } + } + /> + // ( handle_change: NF, - dimensions: Vec, + dimensions: Vec, #[prop(default = false)] is_standalone: bool, context: Vec, #[prop(default = String::new())] heading_sub_text: String, @@ -68,7 +70,7 @@ where }); let handle_select_dropdown_option = - Callback::new(move |selected_dimension: Dimension| { + Callback::new(move |selected_dimension: DimensionWithMandatory| { let dimension_name = selected_dimension.dimension; set_used_dimensions.update(|value: &mut HashSet| { value.insert(dimension_name.clone()); @@ -111,7 +113,7 @@ where .get_value() .into_iter() .map(|ele| (ele.dimension.clone(), ele)) - .collect::>(); + .collect::>(); view! { - + @@ -413,7 +415,7 @@ where {move || { if last_idx.get() != idx { view! { -
+
"&&"
} @@ -438,7 +440,7 @@ where .filter(|dimension| { !used_dimensions.get().contains(&dimension.dimension) }) - .collect::>(); + .collect::>(); view! { , + dimensions: Vec, ) -> Result { let var = &condition.left_operand; // Dimension name let op = &condition.operator; // Operator type @@ -180,7 +181,7 @@ pub fn get_condition_schema( pub fn construct_context( conditions: Vec, - dimensions: Vec, + dimensions: Vec, ) -> Value { if conditions.is_empty() { json!({}) @@ -209,7 +210,9 @@ pub fn construct_context( pub fn construct_request_payload( overrides: Map, conditions: Vec, - dimensions: Vec, + dimensions: Vec, + description: String, + change_reason: String, ) -> Value { // Construct the override section let override_section: Map = overrides; @@ -220,7 +223,9 @@ pub fn construct_request_payload( // Construct the entire request payload let request_payload = json!({ "override": override_section, - "context": context_section + "context": context_section, + "description": description, + "change_reason": change_reason }); request_payload @@ -230,16 +235,25 @@ pub async fn create_context( tenant: String, overrides: Map, conditions: Vec, - dimensions: Vec, + dimensions: Vec, + description: String, + change_reason: String, + org_id: String, ) -> Result { let host = get_host(); let url = format!("{host}/context"); - let request_payload = construct_request_payload(overrides, conditions, dimensions); + let request_payload = construct_request_payload( + overrides, + conditions, + dimensions, + description, + change_reason, + ); let response = request( url, reqwest::Method::PUT, Some(request_payload), - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; @@ -250,17 +264,25 @@ pub async fn update_context( tenant: String, overrides: Map, conditions: Vec, - dimensions: Vec, + dimensions: Vec, + description: String, + change_reason: String, + org_id: String, ) -> Result { let host = get_host(); let url = format!("{host}/context/overrides"); - let request_payload = - construct_request_payload(overrides, conditions, dimensions.clone()); + let request_payload = construct_request_payload( + overrides, + conditions, + dimensions.clone(), + description, + change_reason, + ); let response = request( url, reqwest::Method::PUT, Some(request_payload), - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; diff --git a/crates/frontend/src/components/default_config_form.rs b/crates/frontend/src/components/default_config_form.rs index ca065035..bd28dc75 100644 --- a/crates/frontend/src/components/default_config_form.rs +++ b/crates/frontend/src/components/default_config_form.rs @@ -3,25 +3,32 @@ pub mod utils; use leptos::*; use serde_json::{json, Value}; +use superposition_types::{ + custom_query::PaginationParams, + database::models::cac::{Function, TypeTemplate}, +}; use web_sys::MouseEvent; -use crate::components::alert::AlertType; -use crate::providers::alert_provider::enqueue_alert; -use crate::providers::editor_provider::EditorProvider; -use crate::schema::EnumVariants; -use crate::types::ListFilters; use crate::{ api::{fetch_functions, fetch_types}, components::{ + alert::AlertType, button::Button, dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, input::{Input, InputType}, }, - schema::{JsonSchemaType, SchemaType}, - types::{FunctionsName, TypeTemplate}, + schema::{EnumVariants, JsonSchemaType, SchemaType}, + types::FunctionsName, +}; +use crate::{ + providers::{alert_provider::enqueue_alert, editor_provider::EditorProvider}, + types::{OrganisationId, Tenant}, }; -use self::{types::DefaultConfigCreateReq, utils::create_default_config}; +use self::{ + types::{DefaultConfigCreateReq, DefaultConfigUpdateReq}, + utils::{create_default_config, update_default_config}, +}; #[component] pub fn default_config_form( @@ -32,12 +39,15 @@ pub fn default_config_form( #[prop(default = Value::Null)] config_value: Value, #[prop(default = None)] function_name: Option, #[prop(default = None)] prefix: Option, + #[prop(default = String::new())] description: String, + #[prop(default = String::new())] change_reason: String, handle_submit: NF, ) -> impl IntoView where NF: Fn() + 'static + Clone, { - let tenant_rs = use_context::>().unwrap(); + let tenant_rws = use_context::>().unwrap(); + let org_rws = use_context::>().unwrap(); let (config_key_rs, config_key_ws) = create_signal(config_key); let (config_type_rs, config_type_ws) = create_signal(config_type); @@ -45,34 +55,25 @@ where let (config_value_rs, config_value_ws) = create_signal(config_value); let (function_name_rs, function_name_ws) = create_signal(function_name); let (req_inprogess_rs, req_inprogress_ws) = create_signal(false); + let (description_rs, description_ws) = create_signal(description); + let (change_reason_rs, change_reason_ws) = create_signal(change_reason); - let functions_resource: Resource> = + let functions_resource: Resource<(String, String), Vec> = create_blocking_resource( - move || tenant_rs.get(), - |current_tenant| async move { - match fetch_functions( - ListFilters { - page: None, - count: None, - all: Some(true), - }, - current_tenant, - ) - .await - { - Ok(data) => data.data.into_iter().collect(), - Err(_) => vec![], - } + move || (tenant_rws.get().0, org_rws.get().0), + |(current_tenant, org)| async move { + fetch_functions(&PaginationParams::all_entries(), current_tenant, org) + .await + .map_or_else(|_| vec![], |data| data.data) }, ); let type_template_resource = create_blocking_resource( - move || tenant_rs.get(), - |current_tenant| async move { - match fetch_types(current_tenant, 1, 10000, false).await { - Ok(response) => response.data, - Err(_) => vec![], - } + move || (tenant_rws.get().0, org_rws.get().0), + |(current_tenant, org)| async move { + fetch_types(&PaginationParams::all_entries(), current_tenant, org) + .await + .map_or_else(|_| vec![], |response| response.data) }, ); @@ -100,35 +101,70 @@ where let f_value = config_value_rs.get(); let fun_name = function_name_rs.get(); + let description = description_rs.get(); + let change_reason = change_reason_rs.get(); + + let create_payload = DefaultConfigCreateReq { + key: config_key_rs.get(), + schema: f_schema.clone(), + value: f_value.clone(), + function_name: fun_name.clone(), + description: description.clone(), + change_reason: change_reason.clone(), + }; - let payload = DefaultConfigCreateReq { + let update_payload = DefaultConfigUpdateReq { schema: f_schema, value: f_value, function_name: fun_name, + description, + change_reason, }; let handle_submit_clone = handle_submit.clone(); + let is_edit = edit; spawn_local({ let handle_submit = handle_submit_clone; async move { - let result = create_default_config( - f_name.clone(), - tenant_rs.get(), - payload.clone(), - ) - .await; + let result = if is_edit { + // Call update_default_config when edit is true + update_default_config( + f_name, + tenant_rws.get().0, + update_payload, + org_rws.get().0, + ) + .await + } else { + // Call create_default_config when edit is false + create_default_config( + tenant_rws.get().0, + create_payload, + org_rws.get().0, + ) + .await + }; match result { Ok(_) => { handle_submit(); + let success_message = if is_edit { + "Default config updated successfully!" + } else { + "New default config created successfully!" + }; enqueue_alert( - String::from("New default config created successfully!"), + String::from(success_message), AlertType::Success, 5000, ); } Err(e) => { - logging::error!("An error occurred while trying to create the default config {}", e); + logging::error!( + "An error occurred while trying to {} the default config: {}", + if is_edit { "update" } else { "create" }, + e + ); enqueue_alert(e, AlertType::Error, 5000); } } @@ -158,6 +194,40 @@ where
+
+ +
+ +
+
+ + +
{move || { @@ -188,7 +217,8 @@ where #[component] pub fn test_form(function_name: String, stage: String) -> impl IntoView { - let tenant_rs = use_context::>().unwrap(); + let tenant_rws = use_context::>().unwrap(); + let org_rws = use_context::>().unwrap(); let (error_message, set_error_message) = create_signal(String::new()); let (output_message, set_output_message) = create_signal::>(None); @@ -200,7 +230,8 @@ pub fn test_form(function_name: String, stage: String) -> impl IntoView { event.prevent_default(); logging::log!("Submitting function form"); - let tenant = tenant_rs.get(); + let tenant = tenant_rws.get().0; + let org = org_rws.get().0; let f_function_name = function_name.clone(); let f_val = json!({ "key": key.get(), @@ -213,7 +244,8 @@ pub fn test_form(function_name: String, stage: String) -> impl IntoView { spawn_local({ async move { - let result = test_function(f_function_name, f_stage, f_val, tenant).await; + let result = + test_function(f_function_name, f_stage, f_val, tenant, org).await; match result { Ok(resp) => { diff --git a/crates/frontend/src/components/function_form/types.rs b/crates/frontend/src/components/function_form/types.rs index f4fe5897..047765d1 100644 --- a/crates/frontend/src/components/function_form/types.rs +++ b/crates/frontend/src/components/function_form/types.rs @@ -6,6 +6,7 @@ pub struct FunctionCreateRequest { pub function: String, pub runtime_version: String, pub description: String, + pub change_reason: String, } #[derive(Serialize)] @@ -13,4 +14,5 @@ pub struct FunctionUpdateRequest { pub function: String, pub runtime_version: String, pub description: String, + pub change_reason: String, } diff --git a/crates/frontend/src/components/function_form/utils.rs b/crates/frontend/src/components/function_form/utils.rs index 61f36245..ce42d8cd 100644 --- a/crates/frontend/src/components/function_form/utils.rs +++ b/crates/frontend/src/components/function_form/utils.rs @@ -1,22 +1,28 @@ -use super::types::{FunctionCreateRequest, FunctionUpdateRequest}; +use serde_json::Value; +use superposition_types::database::models::cac::Function; + use crate::{ - types::{FunctionResponse, FunctionTestResponse}, + types::FunctionTestResponse, utils::{construct_request_headers, get_host, parse_json_response, request}, }; -use serde_json::Value; + +use super::types::{FunctionCreateRequest, FunctionUpdateRequest}; pub async fn create_function( function_name: String, function: String, runtime_version: String, description: String, + change_reason: String, tenant: String, -) -> Result { + org_id: String, +) -> Result { let payload = FunctionCreateRequest { function_name, function, runtime_version, description, + change_reason, }; let host = get_host(); @@ -25,7 +31,7 @@ pub async fn create_function( url, reqwest::Method::POST, Some(payload), - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; @@ -37,12 +43,15 @@ pub async fn update_function( function: String, runtime_version: String, description: String, + change_reason: String, tenant: String, -) -> Result { + org_id: String, +) -> Result { let payload = FunctionUpdateRequest { function, runtime_version, description, + change_reason, }; let host = get_host(); @@ -52,7 +61,7 @@ pub async fn update_function( url, reqwest::Method::PATCH, Some(payload), - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; parse_json_response(response).await @@ -63,6 +72,7 @@ pub async fn test_function( stage: String, val: Value, tenant: String, + org_id: String, ) -> Result { let host = get_host(); let url = format!("{host}/function/{function_name}/{stage}/test"); @@ -71,7 +81,7 @@ pub async fn test_function( url, reqwest::Method::PUT, Some(val), - construct_request_headers(&[("x-tenant", &tenant)])?, + construct_request_headers(&[("x-tenant", &tenant), ("x-org-id", &org_id)])?, ) .await?; diff --git a/crates/frontend/src/components/override_form.rs b/crates/frontend/src/components/override_form.rs index be8fd336..faaa0ed0 100644 --- a/crates/frontend/src/components/override_form.rs +++ b/crates/frontend/src/components/override_form.rs @@ -1,16 +1,17 @@ use std::collections::{HashMap, HashSet}; +use leptos::*; +use serde_json::Value; +use superposition_types::database::models::cac::DefaultConfig; +use web_sys::MouseEvent; + use crate::{ components::{ dropdown::{Dropdown, DropdownDirection}, input::{Input, InputType}, }, schema::{EnumVariants, SchemaType}, - types::DefaultConfig, }; -use leptos::*; -use serde_json::Value; -use web_sys::MouseEvent; #[component] fn type_badge(r#type: Option) -> impl IntoView { diff --git a/crates/frontend/src/components/side_nav.rs b/crates/frontend/src/components/side_nav.rs index 8daee4fd..52a9e37c 100644 --- a/crates/frontend/src/components/side_nav.rs +++ b/crates/frontend/src/components/side_nav.rs @@ -1,60 +1,62 @@ +use crate::api::fetch_workspaces; use crate::components::nav_item::NavItem; use crate::components::skeleton::{Skeleton, SkeletonVariant}; -use crate::types::AppRoute; -use crate::utils::{get_tenants, use_url_base}; +use crate::types::{AppRoute, OrganisationId, Tenant}; +use crate::utils::use_url_base; use leptos::*; use leptos_router::{use_location, use_navigate, A}; +use superposition_types::custom_query::PaginationParams; use web_sys::Event; -fn create_routes(tenant: &str) -> Vec { +fn create_routes(org: &str, tenant: &str) -> Vec { let base = use_url_base(); vec![ AppRoute { - key: format!("{base}/admin/{tenant}/experiments"), - path: format!("{base}/admin/{tenant}/experiments"), + key: format!("{base}/admin/{org}/{tenant}/experiments"), + path: format!("{base}/admin/{org}/{tenant}/experiments"), icon: "ri-test-tube-fill".to_string(), label: "Experiments".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/function"), - path: format!("{base}/admin/{tenant}/function"), + key: format!("{base}/admin/{org}/{tenant}/function"), + path: format!("{base}/admin/{org}/{tenant}/function"), icon: "ri-code-box-fill".to_string(), label: "Functions".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/dimensions"), - path: format!("{base}/admin/{tenant}/dimensions"), + key: format!("{base}/admin/{org}/{tenant}/dimensions"), + path: format!("{base}/admin/{org}/{tenant}/dimensions"), icon: "ri-ruler-2-fill".to_string(), label: "Dimensions".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/default-config"), - path: format!("{base}/admin/{tenant}/default-config"), + key: format!("{base}/admin/{org}/{tenant}/default-config"), + path: format!("{base}/admin/{org}/{tenant}/default-config"), icon: "ri-tools-line".to_string(), label: "Default Config".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/overrides"), - path: format!("{base}/admin/{tenant}/overrides"), + key: format!("{base}/admin/{org}/{tenant}/overrides"), + path: format!("{base}/admin/{org}/{tenant}/overrides"), icon: "ri-guide-fill".to_string(), label: "Overrides".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/resolve"), - path: format!("{base}/admin/{tenant}/resolve"), + key: format!("{base}/admin/{org}/{tenant}/resolve"), + path: format!("{base}/admin/{org}/{tenant}/resolve"), icon: "ri-equalizer-fill".to_string(), label: "Resolve".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/types"), - path: format!("{base}/admin/{tenant}/types"), + key: format!("{base}/admin/{org}/{tenant}/types"), + path: format!("{base}/admin/{org}/{tenant}/types"), icon: "ri-t-box-fill".to_string(), label: "Type Templates".to_string(), }, AppRoute { - key: format!("{base}/admin/{tenant}/config/versions"), - path: format!("{base}/admin/{tenant}/config/versions"), + key: format!("{base}/admin/{org}/{tenant}/config/versions"), + path: format!("{base}/admin/{org}/{tenant}/config/versions"), icon: "ri-camera-lens-fill".to_string(), label: "Config Versions".to_string(), }, @@ -68,10 +70,21 @@ pub fn side_nav( //params_map: Memo, ) -> impl IntoView { let location = use_location(); - let tenant_rs = use_context::>().unwrap(); - let tenant_ws = use_context::>().unwrap(); - let (app_routes, set_app_routes) = - create_signal(create_routes(tenant_rs.get_untracked().as_str())); + let tenant_rws = use_context::>().unwrap(); + let org_rws = use_context::>().unwrap(); + let (app_routes, set_app_routes) = create_signal(create_routes( + org_rws.get_untracked().as_str(), + tenant_rws.get_untracked().as_str(), + )); + let workspace_resource = create_blocking_resource( + move || org_rws.get().0, + |org_id| async move { + let filters = PaginationParams::default(); + fetch_workspaces(&filters, &org_id) + .await + .unwrap_or_default() + }, + ); let resolved_path = create_rw_signal(resolved_path); let original_path = create_rw_signal(original_path); @@ -106,7 +119,7 @@ pub fn side_nav( view! { } }>