diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0c4969132 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target +node-modules \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8593e39a9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = false +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +[makefile] + +charset = utf-8 +indent_style = tab +indent_size = 2 diff --git a/.env b/.env deleted file mode 100644 index f69dce4b9..000000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=postgres://utkarsh.pandey:postgres@localhost/config diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..907b62a2b --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +DATABASE_URL=postgres://postgres:docker@dockerdns:5432/config?sslmode=disable +RUST_LOG=debug +AWS_ACCESS_KEY_ID=test +AWS_SECRET_ACCESS_KEY=test +AWS_SESSION_TOKEN=test +AWS_REGION=ap-south-1 +DB_USER=postgres +DB_HOST=dockerdns:5432 +DB_NAME=config +APP_ENV=DEV +ADMIN_TOKEN="12345678" +AWS_REGION_ENDPOINT=http://dockerdns:4566 +ALLOW_SAME_KEYS_OVERLAPPING_CTX=true +ALLOW_DIFF_KEYS_OVERLAPPING_CTX=true +ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX=true +CAC_HOST="http://localhost:8080" +API_HOSTNAME="http://localhost:8080" +CONTEXT_AWARE_CONFIG_VERSION="v0.1.0" +HOSTNAME="---" +MJOS_ALLOWED_ORIGINS=https://potato.in,https://onion.in,http://localhost:8080 +ACTIX_KEEP_ALIVE=120 +MAX_DB_CONNECTION_POOL_SIZE=3 +DASHBOARD_AUTH_ENABLED=false +ENABLE_TENANT_AND_SCOPE=true +TENANT_VALIDATION_ENABLED=false +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,/" +DASHBOARD_AUTH_URL="https://sandbox.portal.juspay.in/ec/v1/authorize" +SERVICE_PREFIX="" +SERVICE_NAME="CAC" \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index d84691250..15b716990 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,33 @@ -# These are some examples of commonly ignored file patterns. -# You should customize this list as applicable to your project. -# Learn more about .gitignore: -# https://www.atlassian.com/git/tutorials/saving-changes/gitignore - # Node artifact files node_modules/ dist/ -build/ - - - -/backend/target - -# Compiled Java class files -*.class - - - -*Cargo.lock - -# Compiled Python bytecode -*.py[cod] # Log files *.log -# Package files -*.jar - -# Maven +# cargo target/ -dist/ - -# JetBrains IDE -.idea/ - -# Unit test reports -TEST*.xml # Generated by MacOS .DS_Store -# Generated by Windows -Thumbs.db - -# Applications -*.app -*.exe -*.war - -# Large media files -*.mp4 -*.tiff -*.avi -*.flv -*.mov -*.wmv - -.package-lock.json - +.env + +# nix build outputs +backend/result +backend/.tmp +backend/logfile +backend/*PGSQL* +backend/.env +.direnv +/result +/result-* +clients/haskell/result +clients/haskell/dist-newstyle +# dev +bacon.toml +docker-compose/localstack/export_cyphers.sh +test_logs +.keep +.vscode +*.session.sql diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 000000000..e9ea2dcd3 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +edition="2018" +max_width=90 diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6a99eb2da..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "files.trimTrailingWhitespace": true -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e9dceb6b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,824 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## v0.39.1 - 2024-04-17 +### Package updates +- cac_client bumped to cac_client-v0.9.1 +- context-aware-config bumped to context-aware-config-v0.26.1 +### Global changes + +- - - + +## v0.39.0 - 2024-04-16 +### Package updates +- cac_client bumped to cac_client-v0.9.0 +- context-aware-config bumped to context-aware-config-v0.26.0 +### Global changes +#### Bug Fixes +- PICAF-26366 fixed error in client - (d1b1f03) - ankit.mahato +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + +## v0.38.2 - 2024-04-12 +### Package updates +- frontend bumped to frontend-v0.5.1 +### Global changes +#### Bug Fixes +- [PICAF-26529] function route fix - (aba54da) - Pratik Mishra + +- - - + +## v0.38.1 - 2024-04-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.2 +### Global changes + +- - - + +## v0.38.0 - 2024-04-10 +### Package updates +- frontend bumped to frontend-v0.5.0 +### Global changes + +- - - + +## v0.37.0 - 2024-04-10 +### Package updates +- service-utils bumped to service-utils-v0.13.0 +### Global changes +#### Features +- [PICAF-25423] added new result, error type and error macros - (e673fb1) - Shubhranshu Sanjeev +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + +## v0.36.1 - 2024-04-08 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.1 +### Global changes + +- - - + +## v0.36.0 - 2024-04-05 +### Package updates +- frontend bumped to frontend-v0.4.0 +### Global changes + +- - - + +## v0.35.0 - 2024-04-05 +### Package updates +- context-aware-config bumped to context-aware-config-v0.25.0 +- experimentation-platform bumped to experimentation-platform-v0.12.0 +- frontend bumped to frontend-v0.3.0 +- cac_client bumped to cac_client-v0.8.0 +### Global changes +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26101] client interface improvements - (d606cb1) - Kartik +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik +#### Miscellaneous Chores +- rename superposition to experimentation - (9efa3cb) - Kartik + +- - - + +## v0.34.2 - 2024-03-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.2 +### Global changes + +- - - + +## v0.34.1 - 2024-03-21 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.1 +### Global changes + +- - - + +## v0.34.0 - 2024-03-21 +### Package updates +- context-aware-config bumped to context-aware-config-v0.24.0 +- frontend bumped to frontend-v0.2.1 +### Global changes + +- - - + +## v0.33.0 - 2024-03-21 +### Package updates +- frontend bumped to frontend-v0.2.0 +### Global changes + +- - - + +## v0.32.2 - 2024-03-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.23.2 +### Global changes +#### Bug Fixes +- PICAF-25598 auto-create variantIds dimension - (ee2e7dc) - ankit.mahato + +- - - + +## v0.32.1 - 2024-03-19 +### Package updates +- context-aware-config bumped to context-aware-config-v0.23.1 +### Global changes + +- - - + +## v0.32.0 - 2024-03-18 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.11.0 +- cac_client bumped to cac_client-v0.7.0 +- superposition_client bumped to superposition_client-v0.5.0 +### Global changes +#### Bug Fixes +- [PICAF-26428] update cargo.lock - (b7aa8b6) - Kartik +- [PICAF-26428] moved to AWS Public ECR for docker images - (26cd710) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + +## v0.31.0 - 2024-03-08 +### Package updates +- external bumped to external-v0.4.0 +- experimentation-platform bumped to experimentation-platform-v0.10.0 +- service-utils bumped to service-utils-v0.12.0 +- context-aware-config bumped to context-aware-config-v0.23.0 +### Global changes + +- - - + +## v0.30.1 - 2024-03-07 +### Package updates +- frontend bumped to frontend-v0.1.1 +### Global changes + +- - - + +## v0.30.0 - 2024-03-06 +### Package updates +- context-aware-config bumped to context-aware-config-v0.22.0 +- caclang bumped to caclang-v0.1.0 +### Global changes +#### Features +- support more operations - (4db2c31) - Kartik Gajendra +- added CAC language support - (c549384) - Kartik Gajendra + +- - - + +## v0.29.0 - 2024-03-06 +### Package updates +- frontend bumped to frontend-v0.1.0 +### Global changes +#### Bug Fixes +- added frontend crate to cog.toml - (c901e21) - Shubhranshu Sanjeev +#### Features +- PICAF-26266 url click and text wrap fixes - (643c54d) - Saurav CV + +- - - + +## v0.28.0 - 2024-03-04 +### Package updates +- cac_client bumped to cac_client-v0.6.0 +- context-aware-config bumped to context-aware-config-v0.21.0 +### Global changes +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + +## v0.27.0 - 2024-03-04 +### Package updates +- context-aware-config bumped to context-aware-config-v0.20.0 +### Global changes +#### Features +- [PICAF-25877 add node to app directory - (9671875) - Pratik Mishra + +- - - + +## v0.26.0 - 2024-02-29 +### Package updates +- context-aware-config bumped to context-aware-config-v0.19.0 +### Global changes +#### Bug Fixes +- autodeploy - (43a4ada) - Kartik +#### Features +- [PICAF-25879] added test,publish api for functions - (050ab24) - Pratik Mishra + +- - - + +## v0.25.0 - 2024-02-28 +### Package updates +- context-aware-config bumped to context-aware-config-v0.18.2 +### Global changes +#### Bug Fixes +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik +- [PICAF-26196] add traffic percentage to experiments table - (5fb0221) - Kartik +#### Features +- autodeploy - (94bc7c7) - Kartik + +- - - + +## v0.24.2 - 2024-02-27 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.4 +### Global changes + +- - - + +## v0.24.1 - 2024-02-26 +### Package updates +- context-aware-config bumped to context-aware-config-v0.18.1 +### Global changes +#### Bug Fixes +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik + +- - - + +## v0.24.0 - 2024-02-22 +### Package updates +- cac_client bumped to cac_client-v0.5.1 +- context-aware-config bumped to context-aware-config-v0.18.0 +### Global changes +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in +#### Features +- PICAF-25876 CRUD APIs for function validator - (7c0c963) - ankit.mahato + +- - - + +## v0.23.0 - 2024-02-22 +### Package updates +- context-aware-config bumped to context-aware-config-v0.17.0 +### Global changes +#### Features +- [PICAF-25877] js eval with node exec - (adc9b19) - Pratik Mishra + +- - - + +## v0.22.0 - 2024-02-21 +### Packages +- context-aware-config locked to context-aware-config-v0.16.0 +- cac_client locked to cac_client-v0.5.0 +- superposition_client locked to superposition_client-v0.4.0 +- service-utils locked to service-utils-v0.11.0 +- external locked to external-v0.3.0 +- experimentation-platform locked to experimentation-platform-v0.9.3 +### Global changes +#### Bug Fixes +- using SERVICE_NAME in is_server instead of SERVER_NAME(wrong var name) - (efe97f0) - Shubhranshu Sanjeev + +- - - + +## v0.21.0 - 2024-02-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.16.0 +- service-utils bumped to service-utils-v0.11.0 +### Global changes +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + +## v0.20.1 - 2024-02-19 +### Package updates +- context-aware-config bumped to context-aware-config-v0.15.2 +### Global changes +#### Bug Fixes +- [PICAF-26004] better logging - (b3d1bc8) - Kartik + +- - - + +## v0.20.0 - 2024-02-15 +### Package updates +- service-utils bumped to service-utils-v0.10.3 +- experimentation-platform bumped to experimentation-platform-v0.9.3 +- context-aware-config bumped to context-aware-config-v0.15.1 +### Global changes +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins +- jenkinsfile now sends build alerts in channel - (2e04ca5) - Kartik +#### Continuous Integration +- pushing cac image to NY sbx ECR - (78c1b32) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (4dc6f19) - Natarajan Kannan +- PICAF-25981: add intro doc and features - (14a7f44) - Natarajan Kannan +- PICAF-25981: add intro doc and features - (0e11056) - Natarajan Kannan +- [PICAF-25981] context aware config docs - (ea04b76) - Kartik +- (PICAF-25983) added setup instruction - (e7d00d9) - Saurav Suman +#### Features +- [PICAF-25981] client-integration-doc - (bc4927d) - Pratik Mishra +- added bool, i64 and decimal in default config form - (fca1ca6) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-25973] autodeploy to sbx - (b812140) - Kartik +- experimentation docs first cut - (d81aea4) - Shubhranshu Sanjeev + +- - - + +## v0.19.0 - 2024-01-31 +### Package updates +- context-aware-config bumped to context-aware-config-v0.15.0 +### Global changes +#### Bug Fixes +- refactored experiment page and fixed experiment edit flow - (b153486) - Shubhranshu Sanjeev +#### Features +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman + +- - - + +## v0.18.1 - 2024-01-29 +### Package updates +- context-aware-config bumped to context-aware-config-v0.14.3 +- experimentation-platform bumped to experimentation-platform-v0.9.2 +### Global changes +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev +#### Continuous Integration +- removing test tenant sqls after ci-test - (d1e42db) - Shubhranshu Sanjeev +- added NY ECR registry push to Jenkins - (51995ae) - Shubhranshu Sanjeev + +- - - + +## v0.18.0 - 2024-01-22 +### Packages +- experimentation-platform locked to experimentation-platform-v0.9.1 +- cac_client locked to cac_client-v0.5.0 +- external locked to external-v0.3.0 +- service-utils locked to service-utils-v0.10.2 +- experimentation_client locked to experimentation_client-v0.4.0 +- context-aware-config locked to context-aware-config-v0.14.2 +### Global changes +#### Bug Fixes +- getting api hostname from env for frontend - (837899d) - Shubhranshu Sanjeev +#### Features +- added between in frontend - (0eb60e5) - Akhilesh Bhadauriya + +- - - + +## v0.17.4 - 2024-01-22 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.1 +### Global changes + +- - - + +## v0.17.3 - 2024-01-22 +### Package updates +- service-utils bumped to service-utils-v0.10.2 +### Global changes +#### Bug Fixes +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev + +- - - + +## v0.17.2 - 2024-01-18 +### Package updates +- service-utils bumped to service-utils-v0.10.1 +- context-aware-config bumped to context-aware-config-v0.14.2 +### Global changes +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev +- refactored DefaultConfig component + fixed edit flow - (f2d38cc) - Shubhranshu Sanjeev +- fixed dimension form edit flow + fixed table component CellFormatter to accept move closures - (9c3a364) - Shubhranshu Sanjeev +#### Refactoring +- using snake case for component fxn names - (19e9aca) - Shubhranshu Sanjeev + +- - - + +## v0.17.1 - 2024-01-12 +### Package updates +- context-aware-config bumped to context-aware-config-v0.14.1 +### Global changes +#### Bug Fixes +- frontend build process - (cbdad01) - Shubhranshu Sanjeev +- fixed build failure due to rust-version - (f689597) - Shubhranshu Sanjeev + +- - - + +## v0.17.0 - 2024-01-04 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.9.0 +- cac_client bumped to cac_client-v0.5.0 +- context-aware-config bumped to context-aware-config-v0.14.0 +- service-utils bumped to service-utils-v0.10.0 +### Global changes +#### Bug Fixes +- fixed tenant hydration bug - (cf0e633) - Saurav Suman +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- cleanup code - (4820f31) - Kartik Gajendra +- minor docs update - (04ab586) - Kartik Gajendra +- UI fixes for demo - (4927766) - Kartik Gajendra +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +- context parsing - (d46ca42) - Kartik Gajendra +- resolve UI bugs - (98695a8) - Kartik Gajendra +- dimensions page updates - (5220b36) - ankit.mahato +#### Features +- added validation inside default config form , formatted dates , added disable feature of edit - (cacf20f) - Saurav Suman +- resolve page with unified UI - (e84eb41) - Kartik Gajendra +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed experiment suspense block , added generic button - (117bfc8) - Saurav Suman +- experiment create form - (91371c0) - Shubhranshu Sanjeev +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working resolve page - (81c83d4) - Kartik Gajendra +- added state changes in the form - (b64a227) - Saurav Suman +- testing create form - (d0a5aea) - Kartik Gajendra +- working experiments page - (81b17dc) - Kartik Gajendra +- experiment UI - (72e19e6) - Kartik Gajendra +- added default config and override screen - (cd4267e) - Saurav Suman +- added default config page - (95e909d) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- override and context form - (553e3ad) - Shubhranshu Sanjeev +- dimensions - (c5e94fa) - ankit.mahato +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +- formatted frontend code - (70f873f) - Shubhranshu Sanjeev +#### Refactoring +- fixed warnings, added redirection for home page and script for setting up the project - (a9ee3bd) - Saurav Suman +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + +## v0.16.3 - 2023-12-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.2 +### Global changes +#### Bug Fixes +- PICAF-24552 - Cac client library changes to consume backend api response - (5998ccd) - Prasanna P + +- - - + +## v0.16.2 - 2023-11-30 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.8.2 +### Global changes +#### Bug Fixes +- allow ramp 0 - (b8d49aa) - Kartik Gajendra + +- - - + +## v0.16.1 - 2023-11-22 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.1 +### Global changes +#### Bug Fixes +- PICAF-25066 sort json while context creation - (3bd7a97) - Pratik Mishra + +- - - + +## v0.16.0 - 2023-11-16 +### Package updates +- context-aware-config bumped to context-aware-config-v0.13.0 +### Global changes +#### Bug Fixes +- fixed deployment ConfigNotFound failure - (be381f1) - Shubhranshu Sanjeev +#### Features +- update default keys - (d6b9992) - ankit.mahato + +- - - + +## v0.15.1 - 2023-11-16 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.8.1 +### Global changes +#### Bug Fixes +- add different auth types for exp requests to CAC - (bd8ae88) - Kartik Gajendra + +- - - + +## v0.15.0 - 2023-11-11 +### Package updates +- context-aware-config bumped to context-aware-config-v0.12.0 +- external bumped to external-v0.3.0 +- cac_client bumped to cac_client-v0.4.0 +- service-utils bumped to service-utils-v0.9.0 +- experimentation_client bumped to experimentation_client-v0.4.0 +- experimentation-platform bumped to experimentation-platform-v0.8.0 +### Global changes +#### Bug Fixes +- failing build due to update of schema.rs file - (131463b) - Shubhranshu Sanjeev +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## v0.14.1 - 2023-11-09 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.7.1 +### Global changes + +- - - + +## v0.14.0 - 2023-11-08 +### Package updates +- external bumped to external-v0.2.0 +- experimentation-platform bumped to experimentation-platform-v0.7.0 +- service-utils bumped to service-utils-v0.8.0 +- context-aware-config bumped to context-aware-config-v0.11.0 +### Global changes +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + +## v0.13.0 - 2023-11-06 +### Package updates +- external bumped to external-v0.1.0 +### Global changes +#### Bug Fixes +- PICAF-25068 added external crate to cocogitto config - (f6f60ef) - Ritick Madaan +- PICAF-25068 x-tenant header added for /config/resolve call in diff - (0e34c31) - Ritick Madaan + +- - - + +## v0.12.1 - 2023-10-31 +### Package updates +- context-aware-config bumped to context-aware-config-v0.10.2 +### Global changes + +- - - + +## v0.12.0 - 2023-10-27 +### Package updates +- context-aware-config bumped to context-aware-config-v0.10.1 +- cac_client bumped to cac_client-v0.3.0 +- service-utils bumped to service-utils-v0.7.1 +- experimentation_client bumped to experimentation_client-v0.3.0 +### Global changes +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + +## v0.11.0 - 2023-10-25 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.6.0 +- service-utils bumped to service-utils-v0.7.0 +- context-aware-config bumped to context-aware-config-v0.10.0 +### Global changes +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + +## v0.10.0 - 2023-10-20 +### Package updates +- context-aware-config bumped to context-aware-config-v0.9.0 +- service-utils bumped to service-utils-v0.6.0 +- experimentation_client bumped to experimentation_client-v0.2.0 +### Global changes + +- - - + +## v0.9.1 - 2023-10-13 +### Package updates +- experimentation_client bumped to experimentation_client-v0.1.3 +### Global changes + +- - - + +## v0.9.0 - 2023-10-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.8.0 +- experimentation-platform bumped to experimentation-platform-v0.5.0 +### Global changes +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev +#### Tests +- added postman test for update override_keys api - (cc96ca1) - Shubhranshu Sanjeev +- added tests for experiment helper fnxs - (ea4db17) - Shubhranshu Sanjeev + +- - - + +## v0.8.1 - 2023-10-10 +### Package updates +- context-aware-config bumped to context-aware-config-v0.7.1 +### Global changes + +- - - + +## v0.8.0 - 2023-10-09 +### Package updates +- context-aware-config bumped to context-aware-config-v0.7.0 +- service-utils bumped to service-utils-v0.5.0 +### Global changes +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan + +- - - + +## v0.7.1 - 2023-10-05 +### Package updates +- service-utils bumped to service-utils-v0.4.1 +- context-aware-config bumped to context-aware-config-v0.6.1 +### Global changes +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra +- ssh.bitbucket.juspay.net added to known hosts in docker bulid - (9207701) - Ritick Madaan + +- - - + +## v0.7.0 - 2023-10-05 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.4.0 +- context-aware-config bumped to context-aware-config-v0.6.0 +### Global changes +#### Continuous Integration +- **(flake.nix)** pin nodejs version to 18 in flake - (614261e) - Natarajan Kannan +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra +- PICAF-24664 cors middleware attached - (8cb4805) - Ritick Madaan + +- - - + +## v0.6.1 - 2023-09-20 +### Package updates +- cac_client bumped to cac_client-v0.2.1 +- context-aware-config bumped to context-aware-config-v0.5.1 +### Global changes + +- - - + +## v0.6.0 - 2023-09-12 +### Package updates +- cac_client bumped to cac_client-v0.2.0 +- service-utils bumped to service-utils-v0.4.0 +- context-aware-config bumped to context-aware-config-v0.5.0 +- experimentation-platform bumped to experimentation-platform-v0.3.1 +### Global changes +#### Bug Fixes +- fixed random timeouts in internal http calls to CAC - (a4e95a3) - Shubhranshu Sanjeev +#### Continuous Integration +- added 20 minutes timeout on pipeline - (2f6bf9e) - Shubhranshu Sanjeev +#### Features +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + +## v0.5.1 - 2023-09-06 +### Package updates +- experimentation_client bumped to experimentation_client-v0.1.2 +### Global changes +#### Bug Fixes +- trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev +#### Continuous Integration +- deleting postgres's docker image on every test - (8a198d6) - Ritick Madaan + +- - - + +## v0.5.0 - 2023-09-06 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.3.0 +### Global changes +#### Features +- [PICAF-24160] record the chosen variant after conclude - (1c3c6e6) - Kartik Gajendra + +- - - + +## v0.4.1 - 2023-09-06 +### Package updates +- experimentation_client bumped to experimentation_client-v0.1.1 +### Global changes +#### Bug Fixes +- fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-24114 updated integ AP tracker curl with new version - (1e0fa5b) - Ritick Madaan + +- - - + +## v0.4.0 - 2023-09-06 +### Package updates +- context-aware-config bumped to context-aware-config-v0.4.0 +- service-utils bumped to service-utils-v0.3.0 +### Global changes +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + +## v0.3.0 - 2023-09-05 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.2.0 +- context-aware-config bumped to context-aware-config-v0.3.0 +- service-utils bumped to service-utils-v0.2.0 +### Global changes +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra +#### Revert +- Revert "fix: PICAF-24114 logged env variable's value before kms decrypting" - (2a935c9) - Ritick Madaan + +- - - + +## v0.2.0 - 2023-09-05 +### Package updates +- context-aware-config bumped to context-aware-config-v0.2.0 +### Global changes +#### Bug Fixes +- PICAF-24273 cleaned up Dockerfile - (cb5c4fb) - Ritick Madaan +- moved git init to separate stage - (9477de6) - Shubhranshu Sanjeev +#### Features +- PICAF-23598 implemented tracing-actix-web for logging - (63dee8c) - Ritick Madaan + +- - - + +## v0.1.0 - 2023-09-01 +### Package updates +- experimentation-platform bumped to experimentation-platform-v0.1.0 +- service-utils bumped to service-utils-v0.1.0 +- cac_client bumped to cac_client-v0.1.0 +- context-aware-config bumped to context-aware-config-v0.1.0 +- experimentation_client bumped to experimentation_client-v0.1.0 +### Global changes +#### Bug Fixes +- PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- moved tables and types under cac_v1 schema - (1be82f1) - Shubhranshu Sanjeev +- added last_modified column and indexes - (942d723) - Shubhranshu Sanjeev +- PICAF-23634 fixed ordering of /context endpoints - (e7b71b3) - Ritick Madaan +- PICAF-22642 moved dimension_type to cac_v1 schema - (5ae90a7) - Ritick Madaan +- PICAF-23520 enabled override updates in PUT /context by deep_merge - (53b148a) - Ritick Madaan +- PICAF-23552 added moved tables to cac_v1 schema - (f9fb191) - Ritick Madaan +- PICAF-23552 database schema url - (e32c1c7) - Ritick Madaan +- PICAF-23552 added search path for schema in database_url - (fd36bdc) - Ritick Madaan +- PICAF-23199 removed unecessary and wrap over conditions - (602643d) - Ritick Madaan +- PICAF-23231 moved override inside contexts table - (e09dd45) - Ritick Madaan +- PICAF-22967: DB password URI encoded. - (c01d921) - Shrey Bana +- removed delete dimension api - (b3b126b) - Ritick Madaan +- storing pre calculated priority in contexts table - (7de363c) - Ritick Madaan +- PICAF-22646 dimension Table scaffolding, PUT<>DELETE apis `/dimension` - (6f4b52e) - Ritick Madaan +- changed host to 0.0.0.0 - (f487a5b) - Ritick Madaan +- base64 decoding kms cypher - (952b546) - Ritick Madaan +- improved log for env not found - (7240266) - Ritick Madaan +- corrected env for DB_PASSWORD and default for AWS_REGION - (a056d85) - Ritick Madaan +#### Build system +- added version tag to docker images - (501bdf4) - Shubhranshu Sanjeev +- PICAF-22783 installing ca-certificates for ssl verification - (626e8e5) - Ritick Madaan +#### Continuous Integration +- added cocogitto config for automatic versioning - (1266bf9) - Shubhranshu Sanjeev +- added postman collection for experimentation-platform - (280441a) - Shubhranshu Sanjeev +- fix newman dev dependency ref - (11e4cbd) - Natarajan Kannan +- PICAF-23646 udpated `docker container ls` filter - (ce93bc0) - Ritick Madaan +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +- PICAF-23646: switch to using newman - (846b931) - Natarajan Kannan +- automated newman test setup - (c2667c0) - Natarajan Kannan +- PICAF-22647: Upgraded nixos to 23.05. - (267dfdc) - Shrey Bana +- moved nixpkgs to nixos-22.11 as the unstable one had broken rustfmt - (0083af7) - Ritick Madaan +- PICAF-22654: Enabling production docker image push. - (76311d7) - Shrey Bana +- PICAF-22654: Commented out prod docker push. - (60b8142) - Shrey Bana +- PICAF-22654: Created pipeline for automated-deployment - (b3208cc) - Shrey Bana +- made some miscellaneous changes for local setup - (89cf0ec) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- [PICAF-24010] added support for CUG in super position client - (4eeae99) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +- added 304 <> last-modified for GET /config - (7592a21) - Saurav Suman +- PICAF-23520 PUT /context/move/{ctx_id} api - (4875284) - Ritick Madaan +- PICAF-22511 added DELETE /context/{ctx_id} api - (d557f84) - Ritick Madaan +- PICAF-22678: Added authentication. - (cd20874) - Shrey Bana +- PICAF-23199 removed properties constraint on objects in schema - (dd83bd5) - Ritick Madaan +- PICAF-23057 rust library - (05f80ec) - Ritick Madaan +- PICAF-22932: Added support for validation via JSON schema. - (51c81cb) - Shrey Bana +- PICAF-22664: Added context list API - (dc21416) - Shrey Bana +- GET /config api - (37ab8e9) - Ritick Madaan +- /default-config/ PUT api - (ae12c7b) - Ritick Madaan +- PICAF-22783 added localstack setup along with kms - (a8d3a15) - Ritick Madaan +- PICAF-22654: Added health-check endpoint. - (993a6d2) - Shrey Bana +- PICAF-22510 Added context fetch API - (7609a68) - Shrey Bana +- context/add api along with db setup - (f5206cb) - Ritick Madaan +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev +- moved AppState & utility fx to new crate - (4f734a5) - Shubhranshu Sanjeev +- PICAF-22469 removed old contexts table - (49770dc) - Ritick Madaan +- PICAF-22468 moved db related modules to db crate - (f7d9492) - Ritick Madaan +#### Tests +- update to latest newman that handles top level events and body lang type - (fcd7724) - Natarajan Kannan +- fix newman version used in tests - (62ede3d) - Natarajan Kannan + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..219b11a76 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4926 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags 1.3.2", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-cors" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b340e9cfa5b08690aae90fb61beb44e9b06f44fe3d0f93781aaa58cfba86245e" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-files" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "askama_escape", + "bitflags 1.3.2", + "bytes", + "derive_more", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", +] + +[[package]] +name = "actix-http" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d223b13fd481fc0d1f83bb12659ae774d9e3601814c68a0bc539731698cca743" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64 0.21.2", + "bitflags 2.3.1", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2 0.4.9", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a6556ddebb638c2358714d853257ed226ece6023ef9364f23f0c70737ea984" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.5", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "serde", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "attribute-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c94f43ede6f25dab1dea046bff84d85dea61bd49aba7a9011ad66c0d449077b" +dependencies = [ + "attribute-derive-macro", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b409e2b2d2dc206d2c0ad3575a93f001ae21a1593e2d0c69b69c308e63f3b422" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.48", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" + +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.6", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" + +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "bytestring" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +dependencies = [ + "bytes", +] + +[[package]] +name = "cac_client" +version = "0.9.1" +dependencies = [ + "actix-web", + "anyhow", + "cbindgen", + "chrono", + "derive_more", + "jsonlogic", + "log", + "once_cell", + "reqwest", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "tokio", +] + +[[package]] +name = "cached" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eb5776f28a149524d1d8623035760b4454ec881e8cf3838fa8d7e1b11254b3" +dependencies = [ + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", +] + +[[package]] +name = "cached_proc_macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "caclang" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "clap 4.3.4", + "derive_more", + "env_logger 0.8.4", + "evalexpr", + "inquire", + "log", + "regex", + "serde", + "strum", + "strum_macros", + "toml 0.8.8", +] + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "cbindgen" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" +dependencies = [ + "clap 3.2.25", + "heck", + "indexmap 1.9.3", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", + "tempfile", + "toml 0.5.11", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +dependencies = [ + "anstream", + "anstyle", + "bitflags 1.3.2", + "clap_lex 0.5.0", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "collection_literals" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml 0.5.11", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const_format" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + +[[package]] +name = "context_aware_config" +version = "0.26.1" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-http", + "actix-web", + "anyhow", + "base64 0.21.2", + "blake3", + "bytes", + "cac_client", + "chrono", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "experimentation_platform", + "external", + "frontend", + "futures", + "futures-util", + "itertools 0.10.5", + "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "log", + "rand", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", + "tracing-log", + "urlencoding", + "uuid", + "valuable", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "cxx" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.48", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashboard-auth" +version = "0.6.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.9.0#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "dotenv", + "env_logger 0.10.0", + "futures-util", + "log", + "reqwest", + "serde", + "serde_json", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0)", +] + +[[package]] +name = "dashboard-auth" +version = "0.6.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "dotenv", + "env_logger 0.10.0", + "futures-util", + "log", + "reqwest", + "serde", + "serde_json", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0)", +] + +[[package]] +name = "derive-where" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146398d62142a0f35248a608f17edf0dde57338354966d6e41d0eb2d16980ccb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diesel" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a532c1f99a0f596f6960a60d1e119e91582b24b39e2d83a190e61262c3ef0c" +dependencies = [ + "bitflags 2.3.1", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "r2d2", + "serde_json", + "uuid", +] + +[[package]] +name = "diesel-derive-enum" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b10c03b954333d05bfd5be1d8a74eae2c9ca77b86e0f1c3a1ea29c49da1d6c2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "diesel_derives" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74398b79d81e52e130d991afeed9c86034bb1b7735f46d2f5bf7deb261d80303" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.48", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "evalexpr" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e757e796a66b54d19fa26de38e75c3351eb7a3755c85d7d181a8c61437ff60c" + +[[package]] +name = "example" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "chrono", + "experimentation_client", + "serde_json", +] + +[[package]] +name = "experimentation_client" +version = "0.5.0" +dependencies = [ + "cbindgen", + "chrono", + "derive_more", + "dotenv", + "jsonlogic", + "log", + "once_cell", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "experimentation_platform" +version = "0.12.0" +dependencies = [ + "actix", + "actix-web", + "anyhow", + "chrono", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "log", + "reqwest", + "rs-snowflake", + "serde", + "serde_json", + "service_utils", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", + "uuid", +] + +[[package]] +name = "external" +version = "0.4.0" +dependencies = [ + "actix", + "actix-web", + "anyhow", + "chrono", + "dashboard-auth 0.6.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git?tag=v1.9.0)", + "dotenv", + "experimentation_platform", + "log", + "reqwest", + "serde", + "serde_json", + "service_utils", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "frontend" +version = "0.5.1" +dependencies = [ + "actix-files", + "actix-web", + "anyhow", + "cfg-if", + "chrono", + "console_error_panic_hook", + "derive_more", + "dotenv", + "futures", + "http", + "js-sys", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "reqwest", + "serde", + "serde_json", + "strum", + "strum_macros", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.0.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", +] + +[[package]] +name = "inquire" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33e7c1ddeb15c9abcbfef6029d8e29f69b52b6d6c891031b88ed91b5065803b" +dependencies = [ + "bitflags 1.3.2", + "crossterm", + "dyn-clone", + "lazy_static", + "newline-converter", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1be380c410bf0595e94992a648ea89db4dd3f3354ba54af206fd2a68cf5ac8e" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonlogic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0699c6109316a5add3d42ae21ae954f207739f12b68914a0d447b9aa45e6f" +dependencies = [ + "serde_json", +] + +[[package]] +name = "jsonschema" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48354c4c4f088714424ddf090de1ff84acc82b2f08c192d46d226ae2529a465" +dependencies = [ + "ahash", + "anyhow", + "base64 0.21.2", + "bytecount", + "clap 4.3.4", + "fancy-regex", + "fraction", + "getrandom", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + +[[package]] +name = "juspay_superposition" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-http", + "actix-web", + "anyhow", + "base64 0.21.2", + "blake3", + "bytes", + "cac_client", + "chrono", + "context_aware_config", + "dashboard-auth 0.6.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git)", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "experimentation_platform", + "external", + "frontend", + "futures", + "futures-util", + "itertools 0.10.5", + "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "log", + "rand", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "tracing-log", + "tracing-utils", + "urlencoding", + "uuid", + "valuable", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leptos" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269ba4ba91ffa73d9559c975e0be17bd4eb34c6b6abd7fdd5704106132d89d2a" +dependencies = [ + "cfg-if", + "leptos_config", + "leptos_dom", + "leptos_macro", + "leptos_reactive", + "leptos_server", + "server_fn", + "tracing", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_actix" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89db4657bdcd28193e9d8cd640ec5d76b55abdf4b16cd5066f1b03f8aea49758" +dependencies = [ + "actix-http", + "actix-web", + "futures", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "leptos_router", + "parking_lot", + "regex", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "leptos_config" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72d8689d54737991831e9b279bb4fba36d27a93aa975c75cd4241d9a4a425ec" +dependencies = [ + "config", + "regex", + "serde", + "thiserror", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad314950d41acb1bfdb8b5924811b2983484a8d6f69a20b834a173a682657ed4" +dependencies = [ + "async-recursion", + "cfg-if", + "drain_filter_polyfill", + "futures", + "getrandom", + "html-escape", + "indexmap 2.0.2", + "itertools 0.12.1", + "js-sys", + "leptos_reactive", + "once_cell", + "pad-adapter", + "paste", + "rustc-hash", + "serde", + "serde_json", + "server_fn", + "smallvec", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f62dcab17728877f2d2f16d2c8a6701c4c5fbdfb4964792924acb0b50529659" +dependencies = [ + "anyhow", + "camino", + "indexmap 2.0.2", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.48", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fddda3a3b768dad90f80fb56ac6e250bc5c60755f8e3df225913aba4364ed7ee" +dependencies = [ + "futures", + "leptos", + "leptos_config", + "leptos_hot_reload", + "leptos_meta", + "tracing", +] + +[[package]] +name = "leptos_macro" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57955d66f624265222444a5c565fea38efa5b0152a1dfc7c060a78e5fb62a852" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.6.0", + "html-escape", + "itertools 0.12.1", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.48", + "tracing", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc25c0f7f14ed5daf42b8d0d273ed790b0449e8ba8cff1c2fa800dc90a75acb" +dependencies = [ + "cfg-if", + "indexmap 2.0.2", + "leptos", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_reactive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f54a525a0edfc8c2bf3ee92aae411800b8b10892c9cd819f8e8a6d4f0d62f3" +dependencies = [ + "base64 0.21.2", + "cfg-if", + "futures", + "indexmap 2.0.2", + "js-sys", + "paste", + "pin-project", + "rustc-hash", + "self_cell", + "serde", + "serde-wasm-bindgen", + "serde_json", + "slotmap", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31087173c60e25c329a1c6786756dd9ee97750b378622df4d780db160a09040" +dependencies = [ + "cached", + "cfg-if", + "gloo-net", + "itertools 0.12.1", + "js-sys", + "lazy_static", + "leptos", + "leptos_integration_utils", + "leptos_meta", + "linear-map", + "lru", + "once_cell", + "percent-encoding", + "regex", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tracing", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "leptos_server" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1517c2024bc47d764e96053e55b927f8a2159e735a0cc47232542b493df9d" +dependencies = [ + "inventory", + "lazy_static", + "leptos_macro", + "leptos_reactive", + "serde", + "server_fn", + "thiserror", + "tracing", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +dependencies = [ + "serde", + "serde_test", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "serde", + "value-bag", +] + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.2", +] + +[[package]] +name = "manyhow" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b76546495d933baa165075b95c0a15e8f7ef75e53f56b19b7144d80fd52bd" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "manyhow-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ba072c0eadade3160232e70893311f1f8903974488096e2eb8e48caba2f0cf1" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.45.0", +] + +[[package]] +name = "mutually_exclusive_features" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b3f656a17a6cbc115b5c7a40c616947d213ba182135b014d6051b73ab6f019" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ce0f250f34a308dcfdbb351f511359857d4ed2134ba715a4eadd46e1ffd617" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pad-adapter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pq-sys" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0052426df997c0cbd30789eb44ca097e3541717a7b8fa36b1c464ee7edebd" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b5abe3fe82fdeeb93f44d66a7b444dedf2e4827defb0a8e69c437b2de2ef94" +dependencies = [ + "quote", + "quote-use-macros", + "syn 2.0.48", +] + +[[package]] +name = "quote-use-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ea44c7e20f16017a76a245bb42188517e13d16dcb1aa18044bc406cdc3f4af" +dependencies = [ + "derive-where", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "reqwest" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "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 = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rs-snowflake" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e60ef3b82994702bbe4e134d98aadca4b49ed04440148985678d415c68127666" + +[[package]] +name = "rstml" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe542870b8f59dd45ad11d382e5339c9a1047cde059be136a7016095bbdefa77" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.48", + "syn_derive", + "thiserror", +] + +[[package]] +name = "rusoto_core" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db30db44ea73551326269adcf7a2169428a054f14faf9e1768f2163494f2fa2" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "hyper-tls", + "lazy_static", + "log", + "rusoto_credential", + "rusoto_signature", + "rustc_version", + "serde", + "serde_json", + "tokio", + "xml-rs", +] + +[[package]] +name = "rusoto_credential" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee0a6c13db5aad6047b6a44ef023dbbc21a056b6dab5be3b79ce4283d5c02d05" +dependencies = [ + "async-trait", + "chrono", + "dirs-next", + "futures", + "hyper", + "serde", + "serde_json", + "shlex", + "tokio", + "zeroize", +] + +[[package]] +name = "rusoto_kms" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1fc19cfcfd9f6b2f96e36d5b0dddda9004d2cbfc2d17543e3b9f10cc38fce8" +dependencies = [ + "async-trait", + "bytes", + "futures", + "rusoto_core", + "serde", + "serde_json", +] + +[[package]] +name = "rusoto_signature" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ae95491c8b4847931e291b151127eccd6ff8ca13f33603eb3d0035ecb05272" +dependencies = [ + "base64 0.13.1", + "bytes", + "chrono", + "digest 0.9.0", + "futures", + "hex", + "hmac", + "http", + "hyper", + "log", + "md-5", + "percent-encoding", + "pin-project-lite", + "rusoto_credential", + "rustc_version", + "serde", + "sha2", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2f49ace1498612d14f7e0b8245519584db8299541dfe31a06374a828d620ab" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c265de965fe48e09ad8899d0ab1ffebdfa1a9914e4de5ff107b07bd94cf7541" +dependencies = [ + "ciborium", + "const_format", + "gloo-net", + "inventory", + "js-sys", + "lazy_static", + "once_cell", + "proc-macro2", + "quote", + "reqwest", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "syn 2.0.48", + "thiserror", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77000541a62ceeec01eef3ee0f86c155c33dac5fae750ad04a40852c6d5469a" +dependencies = [ + "const_format", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "syn 2.0.48", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3353f22e2bcc451074d4feaa37317d9d17dff11d4311928384734ea17ab9ca" +dependencies = [ + "server_fn_macro", + "syn 2.0.48", +] + +[[package]] +name = "service_utils" +version = "0.13.0" +dependencies = [ + "actix", + "actix-web", + "anyhow", + "base64 0.21.2", + "bytes", + "derive_more", + "diesel", + "dotenv", + "env_logger 0.8.4", + "futures-util", + "jsonschema", + "log", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", + "urlencoding", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "superposition" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-files", + "actix-http", + "actix-web", + "anyhow", + "base64 0.21.2", + "blake3", + "bytes", + "cac_client", + "chrono", + "context_aware_config", + "derive_more", + "diesel", + "diesel-derive-enum", + "dotenv", + "env_logger 0.8.4", + "experimentation_platform", + "frontend", + "futures", + "futures-util", + "itertools 0.10.5", + "jsonschema", + "leptos", + "leptos_actix", + "leptos_meta", + "leptos_router", + "log", + "rand", + "reqwest", + "rs-snowflake", + "rusoto_core", + "rusoto_kms", + "rusoto_signature", + "serde", + "serde_json", + "service_utils", + "strum", + "strum_macros", + "superposition_types 0.1.0 (git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git)", + "tracing-log", + "urlencoding", + "uuid", + "valuable", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git?tag=superposition_types-v0.1.0#086131051b61b1a62989019794c4893251d204a6" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "superposition_types" +version = "0.1.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git#086131051b61b1a62989019794c4893251d204a6" +dependencies = [ + "actix", + "actix-web", + "derive_more", + "log", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "sval" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b031320a434d3e9477ccf9b5756d57d4272937b8d22cb88af80b7633a1b78b1" + +[[package]] +name = "sval_buffer" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf7e9412af26b342f3f2cc5cc4122b0105e9d16eb76046cd14ed10106cf6028" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ef628e8a77a46ed3338db8d1b08af77495123cc229453084e47cd716d403cf" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_ref" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703ca1942a984bd0d9b5a4c0a65ab8b4b794038d080af4eb303c71bc6bf22d7c" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830926cd0581f7c3e5d51efae4d35c6b6fc4db583842652891ba2f1bed8db046" +dependencies = [ + "serde", + "sval", + "sval_buffer", + "sval_fmt", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6eef0000c4a12ecdfd7873ea84a8b5aab5e44db72e38e07b028a25386f29a5" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.9", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-actix-web" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe0d5feac3f4ca21ba33496bcb1ccab58cca6412b1405ae80f0581541e0ca78" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "tracing-utils" +version = "0.3.0" +source = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git#cc0cf0267f5fe1f125c7d27e01667e8b2c025a7f" +dependencies = [ + "actix-http", + "actix-web", + "chrono", + "futures", + "log", + "serde", + "serde_json", + "tracing", + "tracing-actix-web", + "tracing-core", + "tracing-serde", + "tracing-subscriber", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typed-builder" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[package]] +name = "utf8-width" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +dependencies = [ + "valuable-derive", +] + +[[package]] +name = "valuable-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d44690c645190cfce32f91a1582281654b2338c6073fa250b0949fd25c55b32" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "xml-rs" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52839dc911083a8ef63efa4d039d1f58b5e409f923e44c80828f206f66e5541c" + +[[package]] +name = "xxhash-rust" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9828b178da53440fa9c766a3d2f73f7cf5d0ac1fe3980c1e5018d899fd19e07b" + +[[package]] +name = "yansi" +version = "1.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..7a20c9bf6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[workspace] +resolver = "2" + +members = [ + "crates/service_utils", + "crates/context_aware_config", + "crates/experimentation_platform", + "crates/external", + "crates/service_utils", + "crates/experimentation_client", + "crates/cac_client", + "crates/experimentation_client_integration_example", + "crates/frontend", + "crates/caclang", + "crates/juspay_superposition", + "crates/superposition", + "crates/superposition_types" + ] + +[[workspace.metadata.leptos]] +name = "cac" +bin-package = "juspay_superposition" +output-name = "frontend" +lib-package = "frontend" +site-root = "target/site" +site-pkg-dir = "pkg" +style-file = "crates/frontend/styles/style.css" +assets-dir = "crates/frontend/assets" + +[workspace.dependencies] +dotenv = "0.15.0" +actix = "0.13.0" +actix-web = "4.5.0" +diesel = { version = "2.1.0", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid", "postgres_backend"] } +env_logger = "0.8" +log = { version="0.4.20", features = ["kv_unstable_serde"] } +serde = {version = "^1", features = ["derive"]} +serde_json = {version = "1.0"} +derive_more = "^0.99" +base64 = "0.21.2" +urlencoding = "~2.1.2" +chrono = { version = "0.4.26", features = ["serde"] } +uuid = {version = "1.3.4", features = ["v4", "serde"]} +reqwest = { version = "0.11.18", features = ["json"]} +jsonschema = "~0.17" +jsonlogic = "0.5.1" +rs-snowflake = "0.6.0" +rusoto_kms = "0.48.0" +rusoto_signature = "0.48.0" +bytes = "1.4.0" +rusoto_core = "0.48.0" +rand = "0.8.5" +once_cell = { version = "1.18.0" } +anyhow = "1.0.75" +strum_macros = "0.25" +strum = "0.25" +blake3 = "1.3.3" +leptos = { version = "0.5.2" } +leptos_meta = { version = "0.5.2" } +leptos_router = { version = "0.5.2" } +thiserror = { version = "1.0.57" } diff --git a/Dockerfile b/Dockerfile index 727bdc660..825d3540e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,62 @@ -FROM rust as planner -WORKDIR /app -RUN cargo install cargo-chef -COPY ./backend . -RUN cargo chef prepare --recipe-path recipe.json +FROM public.ecr.aws/docker/library/rust:1.76.0 as builder +WORKDIR /build +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 18.19.0 -FROM rust as cacher -WORKDIR /app -RUN cargo install cargo-chef -COPY --from=planner /app/recipe.json recipe.json -RUN cargo chef cook --release --recipe-path recipe.json +RUN mkdir -p $NVM_DIR +RUN curl "https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh" | bash \ + && . $NVM_DIR/nvm.sh \ + && nvm install $NODE_VERSION \ + && nvm alias default $NODE_VERSION \ + && nvm use default -FROM rust as builder -COPY ./backend /app -WORKDIR /app -COPY --from=cacher /app/target target -COPY --from=cacher /usr/local/cargo /usr/local/cargo -RUN cargo build --release - -FROM debian:bullseye -RUN apt-get update -RUN apt-get install --yes libpq5 -COPY --from=builder /app/target/release/context-aware-config /app/context-aware-config +ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules +ENV PATH="${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}" +RUN node --version + +COPY . . +RUN mkdir -p ~/.ssh && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +RUN npm ci --loglevel=info +RUN cd crates/context_aware_config/ && npm ci +RUN mkdir -p target/node_modules +RUN cp -a crates/context_aware_config/node_modules target/ + +# building frontend +RUN --mount=type=ssh cd crates/frontend \ + && wasm-pack build --target=web --no-default-features --features=hydrate + +# copying .wasm, .js and .css files to target/site directory +RUN mkdir -p target/site && mkdir -p target/site/pkg +RUN cd crates/frontend \ + && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css +RUN mv crates/frontend/pkg target/site/ +RUN cp -a crates/frontend/assets/. target/site/ +RUN cp .env.example .env +# building backend +RUN --mount=type=ssh cargo build --release + +FROM public.ecr.aws/debian/debian:bookworm-slim WORKDIR /app +ENV NODE_VERSION=18.19.0 + +ARG SOURCE_COMMIT +ARG CONTEXT_AWARE_CONFIG_VERSION + +RUN apt-get update && apt-get install -y libpq5 ca-certificates curl +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +ENV NVM_DIR=/root/.nvm +RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION} +RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION} +ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}" +RUN node --version + -CMD ["/app/context-aware-config"] +COPY --from=builder /build/target/release/juspay_superposition /app/juspay_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 +ENV CONTEXT_AWARE_CONFIG_VERSION=$CONTEXT_AWARE_CONFIG_VERSION +ENV SOURCE_COMMIT=$SOURCE_COMMIT +CMD ["/app/juspay_superposition"] \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 8a56c430c..d60fd42ca 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,38 +1,295 @@ +def jenkinsConsoleUrl = "$env.JOB_URL" + "$env.BUILD_NUMBER" +def slackResponseArray = [] -def createDockerAndPush(version, packageVersion) { - sh('cd backend') - sh('make build -e packageVersion=0.0.2') +def sendSlack(color, message, jenkinsConsoleUrl, slackResponseArray) { + def slackResponse = slackSend channel: "C04QY9BFM7Y", color: color, message: message+ "\n" + "<${jenkinsConsoleUrl} | *JOB-${env.BUILD_NUMBER}*>" + slackResponseArray.each { item -> + slackSend(channel: slackResponse.threadId, color: "#01F2D1" , message: "`${item}`" ) + } +} + +def getRegistryHost(aws_acc_id, region) { + return aws_acc_id + ".dkr.ecr." + region + ".amazonaws.com"; } pipeline { - agent { - label 'sdk' + agent { label 'hypersdk' } + options { + timeout(time: 20, unit: 'MINUTES') + } + environment { + REGION = "ap-south-1"; + REGISTRY_HOST_SBX = getRegistryHost("701342709052", REGION); + REGISTRY_HOST_PROD = getRegistryHost("980691203742", REGION); + REGISTRY_HOST_NY_PROD = getRegistryHost("147728078333", REGION); + REGISTRY_HOST_NY_SBX = getRegistryHost("463356420488", REGION); + AUTOPILOT_HOST_SBX = "autopilot.internal.staging.mum.juspay.net"; + DOCKER_DIND_DNS = "jenkins-newton-dind.jp-internal.svc.cluster.local" + GIT_REPO_NAME = "context-aware-config" + CARGO_NET_GIT_FETCH_WITH_CLI=true + SSH_AUTH_SOCK = """${sh( + returnStdout: true, + script: ''' + eval $(ssh-agent) > /dev/null + ssh-add /home/jenkins/.ssh/id_rsa + echo $SSH_AUTH_SOCK + ''' + )}""" } stages { stage('Checkout') { steps { - scmSkip(deleteBuild: false, skipPattern:'.*\\[skip ci\\].*') script { - if (sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", returnStatus: true) == 0) { - currentBuild.result = 'ABORTED' - error 'Aborting because commit message contains [skip ci]' + isSkipCI = sh(script: "git log -1 --pretty=%B | grep -F -ie '[skip ci]' -e '[ci skip]'", returnStatus: true) + if (isSkipCI == 0) { + env.SKIP_CI = true; + } else { + env.SKIP_CI = false; } + env.COMMIT_HASH = sh(returnStdout: true, script: "git rev-parse --short HEAD").trim() + } + } + } + + stage('Git init') { + steps { + script { + sh 'rm ~/.ssh/known_hosts && ssh-keyscan ssh.bitbucket.juspay.net >> ~/.ssh/known_hosts' + sh 'git remote set-url origin ssh://git@ssh.bitbucket.juspay.net/picaf/${GIT_REPO_NAME}.git' + sh 'git fetch' + sh 'git config user.name ""Jenkins User""' + sh 'git config user.email bitbucket.jenkins.read@juspay.in' + sh 'rustup toolchain uninstall stable && rustup toolchain install 1.76.0' + } + } + } + + stage('Test') { + when { expression { SKIP_CI == 'false' } } + steps { + script { + def fmtResultCode = sh(script: 'cargo fmt --check', returnStatus:true) + if (fmtResultCode != 0){ + error("Code is not formatted properly. Please run 'cargo fmt' and commit the changes.") + } + + def commitResultCode = sh(script: 'cog check --from-latest-tag', returnStatus:true) + if (commitResultCode != 0) { + error("Commit message does not follow Conventional Commits guidelines.") + } + } + + sh 'make ci-test -e DOCKER_DNS=${DOCKER_DIND_DNS}' + } + } + + stage('Get old Version') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + env.COMMIT_MSG="""${sh(returnStdout: true, script: "git log --format=format:%s -1")}""" + env.OLD_SEMANTIC_VERSION="""${sh( + returnStdout: true, + script: ''' + set +x; + cog -v get-version | tr -d "\n" + ''' + )}""" + } + } + } + + stage('Versioning Management') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + sh 'cog bump --auto --skip-ci "[skip ci]"' + } + } + + stage('Pushing release commit and tags') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + sh "git push origin HEAD:${BRANCH_NAME}" + sh "git push origin --tags" + } + } + } + + stage('Get New Version') { + when { + expression { SKIP_CI == 'false' } + branch 'main' + } + steps { + script { + env.COMMIT_HASH = """${sh(returnStdout: true, script: "git rev-parse --short HEAD")}""".trim() + env.NEW_SEMANTIC_VERSION="""${sh( + returnStdout: true, + script: ''' + set +x; + cog -v get-version | tr -d "\n" + ''' + )}""" + echo "New version - ${NEW_SEMANTIC_VERSION}, Old version - ${OLD_SEMANTIC_VERSION}" } } } stage('Build Image') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + script { + slackResponseArray << "COMMIT BUILT : ${COMMIT_HASH}" + slackResponseArray << "NEW_SEMANTIC_VERSION/DOCKER IMAGE TAG : ${NEW_SEMANTIC_VERSION}" + } + sh '''make ci-build -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + SOURCE_COMMIT=${COMMIT_HASH} \ + SSH_AUTH_SOCK=${SSH_AUTH_SOCK} + ''' + } + } + + stage('Push Image To Sandbox Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_SBX} + ''' + } + } + + stage('Push Image To Production Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_PROD} + ''' + } + } + + stage('Push Image To NY Sandbox Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_NY_SBX} + ''' + } + } + + stage('Push Image To NY Production Registry') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + steps { + sh '''make ci-push -e \ + VERSION=${NEW_SEMANTIC_VERSION} \ + REGION=${REGION} \ + REGISTRY_HOST=${REGISTRY_HOST_NY_PROD} + ''' + } + } + + stage('Create Integ Release Tracker') { + when { + expression { SKIP_CI == 'false' } + expression { env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION } + branch 'main' + } + environment { + CREDS = credentials('SDK_SBX_AP_KEY') + COMMIT_MSG = sh(returnStdout: true, script: "git log --format=format:%s -1") + CHANGE_LOG = "Commit message: ${COMMIT_MSG}"; + AUTHOR_NAME = sh(returnStdout: true, script: "git log -1 --pretty=format:'%ae'") + AP_DEPLOY_VERSION = env.NEW_SEMANTIC_VERSION.replace(".", "x") + } steps { - createDockerAndPush('prod', '0.0.2') + sh """curl -v --location --request POST 'https://${AUTOPILOT_HOST_SBX}/release' \ + --header 'Content-Type: application/json' \ + --header 'x-api-key: ${CREDS}' \ + --data-raw '{ + "service": ["CONTEXT_AWARE_CONFIG"], + "release_manager": "jenkins.jenkins", + "release_tag": "", + "new_version": "${AP_DEPLOY_VERSION}", + "docker_image" : "${NEW_SEMANTIC_VERSION}", + "priority" : 0, + "cluster" : "EKS_MUM", + "is_approved": 1, + "is_infra_approved": 1, + "change_log": "${CHANGE_LOG}", + "rollout_strategy": [ + { + "rollout": 100, + "cooloff": 1, + "pods": 1 + } + ], + "description": "${CHANGE_LOG}", + "product": "HYPER_SDK", + "mode" : "AUTO", + "env" : "UAT" + }'; + """ } } stage('Summary') { steps { script { - echo 'Successfully compiled' + echo 'Build Success' } } } } -} + post { + failure { + script { + if (env.BRANCH_NAME == 'main') { + sendSlack("#AA1100", "@channel *BUILD_FAILED* ", jenkinsConsoleUrl, slackResponseArray) + } + } + } + success { + script { + if (env.BRANCH_NAME == 'main' && env.NEW_SEMANTIC_VERSION != env.OLD_SEMANTIC_VERSION) { + sendSlack("#3CF700", "*BUILD SUCCESS*", jenkinsConsoleUrl, slackResponseArray) + } + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 1f894454a..830b2af34 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,24 @@ -# Context Aware Config # - -This README would normally document whatever steps are necessary to get your application up and running. - -### What is this repository for? ### - -* Quick summary -* Version -* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo) - -### How do I get set up? ### - -* Summary of set up -* Configuration -* Dependencies -* Database configuration -* How to run tests -* Deployment instructions - -### Contribution guidelines ### - -* Writing tests -* Code review -* Other guidelines - -### Who do I talk to? ### - -* Repo owner or admin -* Other community or team contact \ No newline at end of file +# Superposition +Superposition is a cloud configuration and experimentation management platform that allows software teams to manage their configuration via a central location, run multi-variate experiments for different configuration values and evaluate results of these experiments and conclude them accordingly. + +The Superposition platform comes with three components: + +* **Context-Aware-Configuration** - a flexible configuration management system that supports contextual overrides for configuration keys +* **Experimentation** - a experimentation management system that allows supplying different configuration values to equal-sized cohorts (facilitating A/B testing) +* **Metrics** - a metrics sub-system that interacts with analytics backends to provide supporting metrics that enable conclusions to be drawn from experiments (TBD) + +## Detailed documentation +1. [Context-Aware-Configuration](docs/context-aware-config.md) +2. [Experimentation](docs/experimentation.md) +3. [Metrics](docs/metrics.md) +4. [Client Context-Aware-Configuration](docs/client-context-aware-configuration.md) +4. [Client Experimentation](docs/client-experimentation.md) +5. [Local setup](docs/setup.md) + +## Key highlights +* **Admin UI** - Out of the box administration (and tools) UI for configurations and experiments +* **Rich API support** - every action on the platform to manage configurations / experiments is supported with an accompanying API +* **Safe configuration changes** - support canary testing for releasing configuration changes using experiments +* **Type/Validation support** - Comprehensive type support using json-schema and custom validator function support for configuration values +* **Multi-tenant support** - a single deployment allows multiple tenants to manage their configurations/experiments in a completely isolated manner +* **Authn/Authz support** - control who can make configuration/experimentation changes \ No newline at end of file diff --git a/backend/Cargo.toml b/backend/Cargo.toml deleted file mode 100644 index 667765e13..000000000 --- a/backend/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "context-aware-config" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# env -dotenv = "0.15.0" -# Https server framework -actix = "0.13.0" -actix-web = "4.0.0" -# To help with generating uuids -uuid = {version = "^0.8", features = ["v4","serde"]} -# To serialize and deserialize objects from json -serde = {version = "^1", features = ["derive"]} -serde_json = {version = "1.0"} -# For logging and debugging -env_logger = "0.8" -log = "^0.4" -# to work with enums -strum_macros = "^0.24" -strum = {version = "^0.24"} -derive_more = "^0.99" -# date and time -chrono = { version = "0.4", features = ["serde"] } -# ORM -diesel = { version = "2.0.2", features = ["postgres", "r2d2", "serde_json", "chrono", "uuid"] } diff --git a/backend/diesel.toml b/backend/diesel.toml deleted file mode 100644 index 88db6ba63..000000000 --- a/backend/diesel.toml +++ /dev/null @@ -1,8 +0,0 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/db/schema.rs" - -[migrations_directory] -dir = "migrations" diff --git a/backend/migrations/00000000000000_diesel_initial_setup/up.sql b/backend/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index 73f54a278..000000000 --- a/backend/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,68 +0,0 @@ --- Setting up DB - -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.last_modified IS NOT DISTINCT FROM OLD.last_modified - ) THEN - NEW.last_modified := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - - - -CREATE TABLE IF NOT EXISTS dimensions ( - dimension VARCHAR NOT NULL, - priority integer NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(dimension) -); - -CREATE TABLE IF NOT EXISTS global_config ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE IF NOT EXISTS overrides ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(key) -); - -CREATE TABLE IF NOT EXISTS contexts ( - key VARCHAR NOT NULL, - value VARCHAR NOT NULL, - last_modified timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY(key) -); - -CREATE TABLE ctxoverrides ( - context_id VARCHAR NOT NULL, - override_id VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(context_id) -); - -SELECT diesel_manage_updated_at('dimensions'); -SELECT diesel_manage_updated_at('global_config'); -SELECT diesel_manage_updated_at('overrides'); -SELECT diesel_manage_updated_at('contexts'); -SELECT diesel_manage_updated_at('ctxoverrides'); diff --git a/backend/scripts/tables.sql b/backend/scripts/tables.sql deleted file mode 100644 index e91923470..000000000 --- a/backend/scripts/tables.sql +++ /dev/null @@ -1,40 +0,0 @@ - -CREATE table dimensions ( - dimension VARCHAR NOT NULL, - priority integer NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(dimension) -); - -CREATE TABLE global_config ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE overrides ( - key VARCHAR NOT NULL, - value json NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE contexts ( - key VARCHAR NOT NULL, - value VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(key) -); - -CREATE TABLE ctxoverrides ( - context_id VARCHAR NOT NULL, - override_id VARCHAR NOT NULL, - last_modified timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_on timestamp with time zone default CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(context_id) -); \ No newline at end of file diff --git a/backend/src/api/derived/config.rs b/backend/src/api/derived/config.rs deleted file mode 100644 index f2b7c4e78..000000000 --- a/backend/src/api/derived/config.rs +++ /dev/null @@ -1,134 +0,0 @@ -// TODO :: Handle errors with appropriate error message - -use std::collections::HashMap; - -use actix_web::{ - Either::Left, - get, - web::{Data, Json}, - HttpRequest, -}; -use serde_json::{to_value, Error, Value}; - -use crate::api::primary::{ - context_overrides::fetch_override_from_ctx_id, - global_config::get_complete_config, - contexts::fetch_context, - overrides::get_override_helper, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{ - DBError, - SomethingWentWrong, - } - }, - hash::string_based_b64_hash, - helpers::{ - create_all_unique_subsets, - split_stringified_key_value_pair, - }, -}; - -use crate::AppState; - -fn default_parsing_error(err: Error) -> AppError{ - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - } -} - - -async fn get_context_overrides_object(state: &Data, query_string: &str) -> Result { - if query_string == "" { - return Ok(Value::default()); - } - -/************************************************************************************************************/ - // ! Optimize this section - let conditions_vector_temp: Vec = - split_stringified_key_value_pair(&query_string) - .iter() - .map(|x| x.join("=")) - .collect(); - - let conditions_vector: Vec<&str> = - conditions_vector_temp - .iter() - .map(|s| &**s) - .collect(); - -/************************************************************************************************************/ - - let keys = create_all_unique_subsets(&conditions_vector); - - let mut override_map = HashMap::new(); - let mut contexts = Vec::new(); - - for item in keys { - // TODO :: Sort query based on key and fetch from DB - // Add the same logic while posting new context - - let key_string = item.to_owned().join("&"); - let hashed_key = string_based_b64_hash(&key_string).to_string(); - - - if let Ok(override_id) = fetch_override_from_ctx_id(&state, &hashed_key).await { - - let fetched_override_value = get_override_helper(&state, override_id.to_owned()).await?; - - override_map.insert(override_id.to_owned(), fetched_override_value); - - contexts.push( - to_value(HashMap::from([ - ("overrideWithKeys", to_value(override_id).map_err(default_parsing_error)?), - ("condition", fetch_context(&state, &hashed_key).await?), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })? - ); - } - } - - to_value(HashMap::from([ - ("context", to_value(contexts).map_err(default_parsing_error)?), - ("overrides", to_value(override_map).map_err(default_parsing_error)?), - ])) - .map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - }) -} - - - - -#[get("")] -pub async fn get_config(state: Data, req: HttpRequest) -> Result, AppError> { - let query_string = req.query_string(); - - let global_config = to_value(get_complete_config(&state).await?).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?; - - let context_overrides = get_context_overrides_object(&state, query_string).await?; - - Ok(Json(to_value(HashMap::from([ - ("global_config", global_config), - ("context_overrides", context_overrides) - ])).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError, - })?)) -} diff --git a/backend/src/api/derived/context_override.rs b/backend/src/api/derived/context_override.rs deleted file mode 100644 index f3f434a71..000000000 --- a/backend/src/api/derived/context_override.rs +++ /dev/null @@ -1,67 +0,0 @@ -// TODO :: Handle errors with appropriate error message -use std::collections::HashMap; - -use actix_web::{ - Either::Left, - post, - web::{Data, Json}, -}; - -use log::info; -use serde::{Serialize, Deserialize}; -use serde_json::{to_value, Value}; - -use crate::utils::errors::{ - AppError, - AppErrorType::{ - SomethingWentWrong, - } -}; - -use crate::api::primary::{ - overrides::add_new_override, - contexts::add_new_context, - context_overrides::add_ctx_override, -}; - - -use crate::{AppState}; - -#[derive(Deserialize, Serialize)] -pub struct KeyValue { - context_value: Value, - override_value: Value, -} - -#[post("")] -pub async fn add_new_context_override(state: Data, body: Json) -> Result, AppError> { - let override_value_from_body = body.override_value.clone(); - let context_value_from_body = body.context_value.clone(); - - - info!("Prithiv {:?}", context_value_from_body); - - // ! Create transaction - // ! TODO:: Fix this asap - - // Ignore even there is an existing context - let context_id = add_new_context(&state, context_value_from_body).await?; - - // Ignore even there is an existing override - let override_id = add_new_override(&state, override_value_from_body.to_owned()).await?; - - add_ctx_override(&state, context_id.clone().id, override_id.clone().id).await?; - - Ok(Json( - to_value( - HashMap::from([ - ("context_id", context_id.id), - ("override_id", override_id.id) - ]) - ).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - })? - )) -} \ No newline at end of file diff --git a/backend/src/api/derived/mod.rs b/backend/src/api/derived/mod.rs deleted file mode 100644 index dfd77c76b..000000000 --- a/backend/src/api/derived/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod config; -pub mod context_override; \ No newline at end of file diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs deleted file mode 100644 index 9ef763b37..000000000 --- a/backend/src/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod primary; -pub mod derived; \ No newline at end of file diff --git a/backend/src/api/primary/context_overrides.rs b/backend/src/api/primary/context_overrides.rs deleted file mode 100644 index 568be5d18..000000000 --- a/backend/src/api/primary/context_overrides.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::collections::HashMap; - -use actix::Addr; -use actix_web::{ - Either::{Left}, - delete, - get, - post, - web::{Data, Json, Path}, -}; -use serde::{Deserialize, Serialize}; -use serde_json::{to_value, Value}; - -use crate::{ - messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchCtxOverrides}, - AppState, DbActor, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{ - DataExists, - DBError, - NotFound - } - }, -}; - -#[derive(Deserialize)] -pub struct BodyType { - context_id: String, - override_id: String -} - -#[derive(Serialize)] -pub struct ContextOverrideResponse { - context_id: String, -} - -// TODO :: Have to re-think and re-implement all these apis -pub async fn add_ctx_override(state: &Data, context_id: String, override_id: String) -> Result, AppError> { - - let db: Addr = state.db.clone(); - - match db - .send(CreateCtxOverrides {context_id, override_id}) - .await - { - Ok(Ok(result)) => Ok(Json(ContextOverrideResponse {context_id: result.context_id})), - Ok(Err(err)) => Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} - -pub async fn fetch_override_from_ctx_id(state: &Data, context_id: &str) -> Result { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(FetchCtxOverrides { - context_id: context_id.to_string() - }) - .await - { - Ok(Ok(result)) => Ok(result.override_id), - Ok(Err(err)) => Err(AppError { - message: Some("failed to fetch key value".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} - -#[post("")] -pub async fn post_ctx_override(state: Data, body: Json) -> Result, AppError> { - let ctx_id: String = body.context_id.clone(); - let ovr_id : String = body.override_id.clone(); - add_ctx_override(&state, ctx_id, ovr_id).await -} - -#[get("/{id}")] -pub async fn get_ctx_override(state: Data, id: Path) -> Result, AppError> { - let context_id = id.to_string(); - let override_id = fetch_override_from_ctx_id(&state, &context_id).await?; - - Ok(Json( - to_value( - HashMap::from([ - ("context_id", context_id), - ("override_id", override_id) - ]) - ).map_err(|err| AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - })? - )) -} - -#[delete("/{id}")] -pub async fn delete_ctx_override(state: Data, id: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(DeleteCtxOverrides { - context_id: id.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(serde_json::Value::String(result.context_id))), - Ok(Err(err)) => Err(AppError { - message: Some("Data not found".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} diff --git a/backend/src/api/primary/contexts.rs b/backend/src/api/primary/contexts.rs deleted file mode 100644 index 30d5b03d0..000000000 --- a/backend/src/api/primary/contexts.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::collections::{ - BTreeMap, - HashMap -}; - -use actix::Addr; -use actix_web::{ - Either::{Left}, - delete, - get, - post, - web::{Data, Json, Path}, -}; -use serde::Serialize; -use serde_json::{from_value, to_value, Error, Value}; - -use crate::{ - messages::contexts::{CreateContext, DeleteContext, FetchContext}, - AppState, DbActor, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{ - DataExists, - NotFound, - DBError, - SomethingWentWrong - } - }, - hash::string_based_b64_hash, - helpers::split_stringified_key_value_pair, -}; - - -#[derive(Serialize, Clone)] -pub struct ContextIdResponse { - pub id: String, -} - -fn default_parsing_error(err: Error) -> AppError{ - AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - } -} - -// TODO :: Implement Range based transforms -fn transform_context(raw_context_value: Value) -> Result { - - // BTreeMap is used to make keys in sorted order - let b_tree: BTreeMap = from_value(raw_context_value).map_err(default_parsing_error)?; - - let mut result: Vec = Vec::new(); - - for (key, value) in b_tree { - let value_object: HashMap = from_value(value).map_err(default_parsing_error)?; - - let operator = value_object.get("operator").map(|val| val.to_string()); - let value = value_object.get("value").map(|val| val.to_string()); - - let max_range = value_object.get("max_range").map(|val| val.to_string()); - let min_range = value_object.get("min_range").map(|val| val.to_string()); - - match (operator.as_deref(), value, max_range, min_range) { - - // ? `==` or `equals` ? - (Some("=="), Some(val), _, _) => result.push(key + "=" + &val), - - // TODO :: Implement Range based transforms properly - // ? Do we need to add inclusive check - (Some("range"), _, Some(max_range_val), Some(min_range_val)) => result.push(min_range_val + "<" + &key + "<" + &max_range_val), - (_, _, _, _) => () - }; - - } - - Ok(result.join("&")) -} - - -pub async fn add_new_context(state: &Data, context_value: Value) -> Result { - let db: Addr = state.db.clone(); - - let transformed_context_value = transform_context(context_value)?; - - // ? TODO :: Post as an array of value - // ? TODO :: Sort query based on key and add to DB - let hashed_value = string_based_b64_hash(&transformed_context_value).to_string(); - - match db - .send(CreateContext { - key: hashed_value, - value: transformed_context_value, - }) - .await - { - Ok(Ok(result)) => Ok(ContextIdResponse {id: result.key}), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to add context".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - } -} - - -fn format_context_json(input: &str) -> Result { - let conditions_vector = split_stringified_key_value_pair(input); - - let mut formatted_conditions_vector = Vec::new(); - - for condition in conditions_vector { - let var_map = to_value(HashMap::from([("var", condition[0])])).map_err(default_parsing_error)?; - let value_as_value = to_value(condition[1]).map_err(default_parsing_error)?; - - let value_arr = vec![[var_map, value_as_value]]; - - // Add range based queries - let condition_map = to_value(HashMap::from([("==", value_arr)])).map_err(default_parsing_error)?; - - formatted_conditions_vector.push(condition_map); - } - - Ok(if formatted_conditions_vector.len() == 1 { - formatted_conditions_vector[0].to_owned() - } else { - to_value(HashMap::from([("and", formatted_conditions_vector)])).map_err(default_parsing_error)? - }) -} - - -pub async fn fetch_context(state: &Data, key: &String) -> Result{ - let db: Addr = state.db.clone(); - - let raw_context_string = match db - .send(FetchContext { - key: key.to_owned() - }) - .await - { - Ok(Ok(result)) => Ok(result.value), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to get context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - - }?; - - format_context_json(&raw_context_string) - -} - -#[post("")] -pub async fn post_context(state: Data, body: Json) -> Result, AppError> { - let context_value = body.clone(); - Ok(Json(add_new_context(&state, context_value).await?)) -} - -#[get("/{key}")] -pub async fn get_context(state: Data, id: Path) -> Result, AppError> { - Ok(Json(fetch_context(&state, &id.to_string()).await?)) -} - -#[delete("/{key}")] -pub async fn delete_context(state: Data, key: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(DeleteContext { - key: key.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to remove context".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - - } -} diff --git a/backend/src/api/primary/dimensions.rs b/backend/src/api/primary/dimensions.rs deleted file mode 100644 index f07bd0369..000000000 --- a/backend/src/api/primary/dimensions.rs +++ /dev/null @@ -1,105 +0,0 @@ -use actix_web:: { - Either::{Left}, - get, - post, - web::{Path, Json, Data}, -}; -use crate::models::db_models::Dimension; -use serde::{Serialize, Deserialize}; - -use crate::{ - messages::dimensions::{ - FetchDimensions, - FetchDimension, - CreateDimension - }, AppState, DbActor}; - -use actix::Addr; - -use crate::utils::errors::{ - AppError, - AppErrorType::{ - DBError, - NotFound, - DataExists - } -}; - -// Get dimension table -#[get("")] -pub async fn get_dimensions(state: Data) -> Result>, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db.send(FetchDimensions).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to get dimensions".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) - } - -} - -// Get request to fetch dimension from dimension name -#[derive(Deserialize, Serialize)] -pub struct Key { - dimension: String, -} - -#[get("/{dimension}")] -pub async fn get_dimension_key(state: Data, params: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - let dimension_key = params.into_inner().dimension; - - match db.send(FetchDimension {dimension: dimension_key}).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to get required dimension".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) - } -} - - -// Post request to add key, value to dimension table -#[derive(Deserialize, Serialize, Clone)] -pub struct KeyValue { - dimension: String, - priority: i32, -} - -#[post("")] -pub async fn post_dimension(state: Data, body: Json) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db.send(CreateDimension { - dimension: body.dimension.clone(), - priority: body.priority - }).await { - - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to add dimension".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) - } -} - diff --git a/backend/src/api/primary/global_config.rs b/backend/src/api/primary/global_config.rs deleted file mode 100644 index 9dd15880d..000000000 --- a/backend/src/api/primary/global_config.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::collections::HashMap; - -use actix_web:: { - Either::{Left}, - get, - post, - web::{Path, Json, Data}, -}; -use serde_json::{Value, to_value}; -use crate::models::db_models::GlobalConfig; -use serde::{Serialize, Deserialize}; -use crate::{ - messages::global_config::{ - FetchGlobalConfig, - FetchConfigKey, - CreateGlobalKey, - }, AppState, DbActor}; -use actix::Addr; -use crate::utils::errors::{ - AppError, - AppErrorType::{ - SomethingWentWrong, - DBError, - NotFound, - DataExists - } -}; - -async fn get_all_rows_from_global_config(state: &Data) -> Result, AppError> { - let db: Addr = state.db.clone(); - match db.send(FetchGlobalConfig).await { - Ok(Ok(result)) => Ok(result), - Ok(Err(err)) => Err(AppError { - message: Some("config not found".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }), - } -} - -pub async fn get_complete_config(state: &Data) -> Result, AppError> { - let db_rows = get_all_rows_from_global_config(&state).await?; - let mut hash_map: HashMap = HashMap::new(); - - for row in db_rows { - hash_map.insert(row.key, row.value); - } - - match to_value(&hash_map) { - Ok(res) => if hash_map.keys().len() == 0 { - Err(AppError { - message: Some("failed to get global config".to_string()), - cause: Some(Left("global config doesn't exist".to_string())), - status: NotFound - }) - } else { - Ok(Json(res)) - }, - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - }) - - } -} - - -// Get whole global config -#[get("")] -pub async fn get_global_config(state: Data) -> Result, AppError> { - get_complete_config(&state).await -} - -// Get request to fetch value for given key -#[derive(Deserialize, Serialize)] -pub struct Key { - key: String, -} - -#[get("/{key}")] -pub async fn get_global_config_key(state: Data, params: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - let key = params.into_inner().key; - - match db.send(FetchConfigKey {key}).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to fetch global config".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} - - -// Post request to add key, value -#[derive(Deserialize, Serialize)] -pub struct KeyValue { - key: String, - value: Value, -} - -#[post("")] -pub async fn post_config_key_value(state: Data, body: Json) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db.send(CreateGlobalKey { - key: body.key.clone(), - value: body.value.clone() - }).await { - Ok(Ok(result)) => Ok(Json(result)), - Ok(Err(err)) => Err(AppError { - message: Some("failed to add new key to global config".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError { - message: None, - cause: Some(Left(err.to_string())), - status: DBError - }) - } - -} diff --git a/backend/src/api/primary/mod.rs b/backend/src/api/primary/mod.rs deleted file mode 100644 index e7a452044..000000000 --- a/backend/src/api/primary/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod global_config; -pub mod dimensions; -pub mod overrides; -pub mod contexts; -pub mod context_overrides; diff --git a/backend/src/api/primary/overrides.rs b/backend/src/api/primary/overrides.rs deleted file mode 100644 index 58562a713..000000000 --- a/backend/src/api/primary/overrides.rs +++ /dev/null @@ -1,147 +0,0 @@ -use actix::Addr; -use actix_web::{ - delete, - get, - post, - web::{Data, Json, Path}, - Either::{Left, Right} -}; -use serde::Serialize; -use serde_json::{Value, to_value}; - -use crate::{ - messages::overrides::{CreateOverride, DeleteOverride, FetchOverride}, - AppState, DbActor, -}; - -use crate::utils::{ - errors::{ - AppError, - AppErrorType::{ - BadRequest, - DataExists, - DBError, - SomethingWentWrong, - NotFound - } - }, - hash::string_based_b64_hash, - helpers::sort_multi_level_keys_in_stringified_json, - validations::validate_sub_tree, -}; - -use crate::api::primary::global_config::get_complete_config; - -#[derive(Serialize, Clone)] -pub struct OverrideIdResponse { - pub id: String, -} - - -pub async fn add_new_override(state: &Data, override_value: Value) -> Result { - let db: Addr = state.db.clone(); - - let global_config = - get_complete_config(&state).await - .map_err(|err| AppError { - message: Some("Unable to fetch global config for validation".to_string()), - cause: Some(Left(err.to_string())), - status: DBError - })?; - - - let global_config_as_value = - // TODO :: Discuss and fix this - to_value(global_config.get("global")) - .map_err(|err| AppError { - message: Some("Unable to parse global config for validation".to_string()), - cause: Some(Left(err.to_string())), - status: SomethingWentWrong - })?; - - if let Err(error_message) = validate_sub_tree(&global_config_as_value, &override_value) { - return Err(AppError { - message: Some("Validation failed".to_string()), - cause: Some(Right(error_message)), - status: BadRequest - }) - } - - - // TODO :: Post as an array of value - let formatted_value = - sort_multi_level_keys_in_stringified_json(override_value) - // TODO :: Fix this properly - // .ok_or(OverrideError::ErrorOnParsingBody {error_message : to_value("Error on sorting keys".to_string())})?; - .ok_or(AppError { - message: Some("Unable to parse override value".to_string()), - cause: None, - status: SomethingWentWrong - })?; - - let hashed_value = string_based_b64_hash((&formatted_value).to_string()).to_string(); - - match db - .send(CreateOverride { - key: hashed_value, - value: formatted_value, - }) - .await - { - Ok(Ok(result)) => Ok(OverrideIdResponse {id: result.key}), - Ok(Err(err)) => Err(AppError { - message: Some("Data already exists".to_string()), - cause: Some(Left(err.to_string())), - status: DataExists - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} - -#[post("")] -pub async fn post_override(state: Data, body: Json) -> Result, AppError> { - let override_value = body.clone(); - Ok(Json(add_new_override(&state, override_value).await?)) -} - -pub async fn get_override_helper(state: &Data, key: String) -> Result, AppError> { - let db: Addr = state.db.clone(); - - match db - .send(FetchOverride {key}) - .await - { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("Failed to fetch value for given override key".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} - -#[get("/{key}")] -pub async fn get_override(state: Data, key: Path) -> Result, AppError> { - get_override_helper(&state, key.to_owned()).await -} - -#[delete("/{key}")] -pub async fn delete_override(state: Data, id: Path) -> Result, AppError> { - let db: Addr = state.as_ref().db.clone(); - - match db - .send(DeleteOverride { - key: id.to_string(), - }) - .await - { - Ok(Ok(result)) => Ok(Json(result.value)), - Ok(Err(err)) => Err(AppError { - message: Some("Data not found".to_string()), - cause: Some(Left(err.to_string())), - status: NotFound - }), - Err(err) => Err(AppError {message: None, cause: Some(Left(err.to_string())), status: DBError}) - } -} diff --git a/backend/src/db/schema.rs b/backend/src/db/schema.rs deleted file mode 100644 index 33e66e2fc..000000000 --- a/backend/src/db/schema.rs +++ /dev/null @@ -1,54 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - contexts (key) { - key -> Varchar, - value -> Varchar, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - ctxoverrides (context_id) { - context_id -> Varchar, - override_id -> Varchar, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - dimensions (dimension) { - dimension -> Varchar, - priority -> Int4, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - global_config (key) { - key -> Varchar, - value -> Json, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::table! { - overrides (key) { - key -> Varchar, - value -> Json, - last_modified -> Timestamptz, - created_on -> Timestamptz, - } -} - -diesel::allow_tables_to_appear_in_same_query!( - contexts, - ctxoverrides, - dimensions, - global_config, - overrides, -); diff --git a/backend/src/db/utils.rs b/backend/src/db/utils.rs deleted file mode 100644 index 1de5ba484..000000000 --- a/backend/src/db/utils.rs +++ /dev/null @@ -1,21 +0,0 @@ -use actix::{Actor, Addr, SyncContext}; -use diesel::{ - PgConnection, - r2d2::{ConnectionManager, Pool} -}; - -pub struct AppState { - pub db: Addr -} - -pub struct DbActor(pub Pool>); - -impl Actor for DbActor { - type Context = SyncContext; -} - -pub fn get_pool(db_url: &str) -> Pool> { - let manager: ConnectionManager = ConnectionManager::::new(db_url); - Pool::builder().build(manager).expect("Error building a connection pool") -} - diff --git a/backend/src/handlers/context_overrides.rs b/backend/src/handlers/context_overrides.rs deleted file mode 100644 index a717a9a24..000000000 --- a/backend/src/handlers/context_overrides.rs +++ /dev/null @@ -1,50 +0,0 @@ -use diesel::QueryResult; - -use crate::db::utils::DbActor; -use crate::models::db_models::CtxOverrides; - -use crate::db::schema::ctxoverrides::dsl::*; -use crate::messages::context_overrides::{CreateCtxOverrides, DeleteCtxOverrides, FetchCtxOverrides}; -use actix::Handler; -use diesel::{self, prelude::*}; - -use crate::models::insertables::context_overrides::CtxOverrideInsertion; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating context override"); - - diesel::insert_into(ctxoverrides) - .values(CtxOverrideInsertion { - context_id: msg.context_id, - override_id: msg.override_id - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); - - ctxoverrides - .filter(context_id.eq(msg.context_id)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteCtxOverrides, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching context override"); - - diesel::delete(ctxoverrides) - .filter(context_id.eq(msg.context_id)) - .get_result::(&mut conn) - } -} \ No newline at end of file diff --git a/backend/src/handlers/contexts.rs b/backend/src/handlers/contexts.rs deleted file mode 100644 index a206d5276..000000000 --- a/backend/src/handlers/contexts.rs +++ /dev/null @@ -1,50 +0,0 @@ -use diesel::QueryResult; - -use crate::db::utils::DbActor; -use crate::models::db_models::Contexts; - -use crate::db::schema::contexts::dsl::*; -use crate::messages::contexts::{CreateContext, DeleteContext, FetchContext}; -use actix::Handler; -use diesel::{self, prelude::*}; - -use crate::models::insertables::contexts::NewContext; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating override"); - - diesel::insert_into(contexts) - .values(NewContext { - key: msg.key, - value: msg.value, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); - - contexts - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteContext, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); - - diesel::delete(contexts) - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} diff --git a/backend/src/handlers/dimensions.rs b/backend/src/handlers/dimensions.rs deleted file mode 100644 index 3f491d003..000000000 --- a/backend/src/handlers/dimensions.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::models::db_models::Dimension; -use crate::db::utils::DbActor; - -use crate::db::schema::dimensions::dsl::*; -use crate::messages::dimensions::{ - FetchDimensions, - FetchDimension, - CreateDimension -}; -use actix::Handler; -use diesel::{self, prelude::*}; -use crate::models::insertables::dimensions::NewDimension; - -impl Handler for DbActor { - - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchDimensions, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimensions: Unable to establish connection"); - - dimensions.get_results::(&mut conn) - } -} - - -impl Handler for DbActor { - - type Result = QueryResult; - - fn handle(&mut self, msg: FetchDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimension: Unable to establish connection"); - - dimensions.filter(dimension.eq(msg.dimension)).get_result::(&mut conn) - } -} - - -impl Handler for DbActor { - - type Result = QueryResult; - - fn handle(&mut self, msg: CreateDimension, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Create Dimension: Unable to establish connection"); - - let new_dimension = NewDimension { - dimension: msg.dimension, - priority: msg.priority - }; - - diesel::insert_into(dimensions) - .values(new_dimension) - .get_result::(&mut conn) - } -} diff --git a/backend/src/handlers/global_config.rs b/backend/src/handlers/global_config.rs deleted file mode 100644 index b08cecb9b..000000000 --- a/backend/src/handlers/global_config.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::models::db_models::GlobalConfig; -use crate::db::utils::DbActor; - -use crate::db::schema::global_config::dsl::*; -use crate::messages::global_config::{ - FetchGlobalConfig, - FetchConfigKey, - CreateGlobalKey -}; -use actix::Handler; -use diesel::{self, prelude::*}; -use crate::models::insertables::global_config::NewGlobalConfigKey; - -impl Handler for DbActor { - - type Result = QueryResult>; - - fn handle(&mut self, _msg: FetchGlobalConfig, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch GlobalConfig: Unable to establish connection"); - - global_config.get_results::(&mut conn) - } -} - - -impl Handler for DbActor { - - type Result = QueryResult; - - fn handle(&mut self, msg: FetchConfigKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Fetch Dimension: Unable to establish connection"); - - global_config.filter(key.eq(msg.key)).get_result::(&mut conn) - } -} - - -impl Handler for DbActor { - - type Result = QueryResult; - - fn handle(&mut self, msg: CreateGlobalKey, _ctx: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Create Dimension: Unable to establish connection"); - - let new_key = NewGlobalConfigKey { - key: msg.key, - value: msg.value - }; - - diesel::insert_into(global_config) - .values(new_key) - .get_result::(&mut conn) - } -} diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs deleted file mode 100644 index a79c4e1a4..000000000 --- a/backend/src/handlers/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod dimensions; -pub mod global_config; -pub mod overrides; -pub mod contexts; -pub mod context_overrides; diff --git a/backend/src/handlers/overrides.rs b/backend/src/handlers/overrides.rs deleted file mode 100644 index 1825dba15..000000000 --- a/backend/src/handlers/overrides.rs +++ /dev/null @@ -1,50 +0,0 @@ -use diesel::QueryResult; - -use crate::db::utils::DbActor; -use crate::models::db_models::Overrides; - -use crate::db::schema::overrides::dsl::*; -use crate::messages::overrides::{CreateOverride, DeleteOverride, FetchOverride}; -use actix::Handler; -use diesel::{self, prelude::*}; - -use crate::models::insertables::overrides::NewOverride; - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: CreateOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for creating override"); - - diesel::insert_into(overrides) - .values(NewOverride { - key: msg.key, - value: msg.value, - }) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: FetchOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); - - overrides - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} - -impl Handler for DbActor { - type Result = QueryResult; - - fn handle(&mut self, msg: DeleteOverride, _: &mut Self::Context) -> Self::Result { - let mut conn = self.0.get().expect("Error on making DB connection for fetching override"); - - diesel::delete(overrides) - .filter(key.eq(msg.key)) - .get_result::(&mut conn) - } -} diff --git a/backend/src/main.rs b/backend/src/main.rs deleted file mode 100644 index 263caba77..000000000 --- a/backend/src/main.rs +++ /dev/null @@ -1,115 +0,0 @@ -mod api; -mod models; -mod db; -mod messages; -mod handlers; -mod utils; - -use api::primary::{ - global_config::{ - get_global_config_key, - get_global_config, - post_config_key_value, - }, - - dimensions::{ - get_dimensions, - get_dimension_key, - post_dimension - }, - overrides::{ - post_override, - delete_override, - get_override, - }, - contexts::{ - post_context, - delete_context, - get_context - }, - context_overrides::{ - post_ctx_override, - delete_ctx_override, - get_ctx_override, - } -}; - -use api::derived::{ - config::get_config, - context_override::add_new_context_override -}; - -// use crate::utils::validations::just_for_test; - -use dotenv; -use std::env; -use std::io::Result; - -use db::utils::{get_pool, AppState, DbActor}; -use actix::SyncArbiter; -use actix_web::{HttpServer, App, web::scope, middleware::Logger,web::Data}; - - -#[actix_web::main] -async fn main() -> Result<()> { - // just_for_test(); - dotenv::dotenv().ok(); - std::env::set_var("RUST_LOG", "debug"); - std::env::set_var("RUST_BACKTRACE", "1"); - env_logger::init(); - let db_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set in environment"); - let pool = get_pool(&db_url); - let db_addr = SyncArbiter::start(5, move || DbActor(pool.clone())); - HttpServer::new(move || { - let logger: Logger = Logger::default(); - App::new() - .app_data(Data::new(AppState {db: db_addr.clone()})) - .wrap(logger) - -/***************************** Primary api routes *****************************/ - .service( - scope("/global_config") - .service(get_global_config) - .service(get_global_config_key) - .service(post_config_key_value) - ) - .service( - scope("/dimensions") - .service(get_dimensions) - .service(get_dimension_key) - .service(post_dimension) - ) - .service( - scope("/context_overrides") - .service(post_ctx_override) - .service(delete_ctx_override) - .service(get_ctx_override) - ) - .service( - scope("/override") - .service(post_override) - .service(delete_override) - .service(get_override) - ).service( - scope("/context") - .service(post_context) - .service(delete_context) - .service(get_context) - - ) - -/***************************** Derived api routes *****************************/ - .service( - scope("/config") - .service(get_config) - ) - .service( - scope("add_context_overrides") - .service(add_new_context_override) - ) - }) - .bind(("localhost", 8080))? - .workers(5) - .run() - .await -} diff --git a/backend/src/messages/context_overrides.rs b/backend/src/messages/context_overrides.rs deleted file mode 100644 index 793d636a7..000000000 --- a/backend/src/messages/context_overrides.rs +++ /dev/null @@ -1,23 +0,0 @@ - -use actix::Message; -use crate::models::db_models::CtxOverrides; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct CreateCtxOverrides { - pub context_id: String, - pub override_id: String -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct FetchCtxOverrides { - pub context_id: String, -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct DeleteCtxOverrides { - pub context_id: String, -} \ No newline at end of file diff --git a/backend/src/messages/contexts.rs b/backend/src/messages/contexts.rs deleted file mode 100644 index bd3c2b038..000000000 --- a/backend/src/messages/contexts.rs +++ /dev/null @@ -1,23 +0,0 @@ - -use actix::Message; -use crate::models::db_models::Contexts; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct CreateContext { - pub key: String, - pub value: String, -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct FetchContext { - pub key: String, -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct DeleteContext { - pub key: String, -} diff --git a/backend/src/messages/dimensions.rs b/backend/src/messages/dimensions.rs deleted file mode 100644 index 2c4f9614a..000000000 --- a/backend/src/messages/dimensions.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::models::db_models::Dimension; -use actix::Message; -use diesel::QueryResult; - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchDimensions; - - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchDimension { - pub dimension: String -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateDimension { - pub dimension: String, - pub priority: i32 -} diff --git a/backend/src/messages/global_config.rs b/backend/src/messages/global_config.rs deleted file mode 100644 index b5f3beb38..000000000 --- a/backend/src/messages/global_config.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::models::db_models::GlobalConfig; -use actix::Message; -use diesel::QueryResult; -use serde_json::Value; - -#[derive(Message)] -#[rtype(result = "QueryResult>")] -pub struct FetchGlobalConfig; - - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct FetchConfigKey { - pub key: String -} - -#[derive(Message)] -#[rtype(result = "QueryResult")] -pub struct CreateGlobalKey { - pub key: String, - pub value: Value -} diff --git a/backend/src/messages/mod.rs b/backend/src/messages/mod.rs deleted file mode 100644 index a79c4e1a4..000000000 --- a/backend/src/messages/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod dimensions; -pub mod global_config; -pub mod overrides; -pub mod contexts; -pub mod context_overrides; diff --git a/backend/src/messages/overrides.rs b/backend/src/messages/overrides.rs deleted file mode 100644 index 9c101a5d8..000000000 --- a/backend/src/messages/overrides.rs +++ /dev/null @@ -1,24 +0,0 @@ - -use actix::Message; -use crate::models::db_models::Overrides; -use diesel::QueryResult; -use serde_json::Value; - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct CreateOverride { - pub key: String, - pub value: Value, -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct FetchOverride { - pub key: String, -} - -#[derive(Message)] -#[rtype(result="QueryResult")] -pub struct DeleteOverride { - pub key: String, -} diff --git a/backend/src/models/db_models.rs b/backend/src/models/db_models.rs deleted file mode 100644 index 56c88cd10..000000000 --- a/backend/src/models/db_models.rs +++ /dev/null @@ -1,58 +0,0 @@ - - -use chrono::DateTime; -use chrono::offset::Utc; -use diesel::{Identifiable, Queryable}; -use serde::Serialize; -use serde_json::Value; -use crate::db::schema::{ctxoverrides,contexts, dimensions, global_config, overrides}; - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = dimensions)] -#[diesel(primary_key(dimension))] -pub struct Dimension { - pub dimension: String, - pub priority: i32, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = global_config)] -#[diesel(primary_key(key))] -pub struct GlobalConfig { - pub key: String, - pub value: Value, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = overrides)] -#[diesel(primary_key(key))] -pub struct Overrides { - pub key: String, - pub value: Value, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = contexts)] -#[diesel(primary_key(key))] -pub struct Contexts { - pub key: String, - pub value: String, - pub last_modified: DateTime, - pub created_on: DateTime, -} - -#[derive(Queryable, Debug, Identifiable, Serialize)] -#[diesel(table_name = ctxoverrides)] -#[diesel(primary_key(context_id))] -pub struct CtxOverrides { - pub context_id: String, - pub override_id: String, - pub last_modified: DateTime, - pub created_on: DateTime, -} diff --git a/backend/src/models/insertables/context_overrides.rs b/backend/src/models/insertables/context_overrides.rs deleted file mode 100644 index 1f5252cc5..000000000 --- a/backend/src/models/insertables/context_overrides.rs +++ /dev/null @@ -1,11 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; - -use crate::db::schema::ctxoverrides; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = ctxoverrides)] -pub struct CtxOverrideInsertion { - pub context_id : String, - pub override_id : String -} \ No newline at end of file diff --git a/backend/src/models/insertables/contexts.rs b/backend/src/models/insertables/contexts.rs deleted file mode 100644 index 5653807a7..000000000 --- a/backend/src/models/insertables/contexts.rs +++ /dev/null @@ -1,11 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; - -use crate::db::schema::contexts; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = contexts)] -pub struct NewContext { - pub key : String, - pub value : String, -} diff --git a/backend/src/models/insertables/dimensions.rs b/backend/src/models/insertables/dimensions.rs deleted file mode 100644 index 09c4caf6f..000000000 --- a/backend/src/models/insertables/dimensions.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::db::schema::dimensions; -use diesel::Insertable; -use serde::Serialize; - -#[derive(Insertable, Serialize, Clone)] -#[diesel(table_name=dimensions)] -pub struct NewDimension { - pub dimension: String, - pub priority: i32, -} diff --git a/backend/src/models/insertables/global_config.rs b/backend/src/models/insertables/global_config.rs deleted file mode 100644 index 7d965d6d5..000000000 --- a/backend/src/models/insertables/global_config.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::db::schema::global_config; -use diesel::Insertable; -use serde::Serialize; -use serde_json::Value; - -#[derive(Insertable, Serialize, Clone)] -#[diesel(table_name=global_config)] -pub struct NewGlobalConfigKey { - pub key: String, - pub value: Value, -} diff --git a/backend/src/models/insertables/mod.rs b/backend/src/models/insertables/mod.rs deleted file mode 100644 index a79c4e1a4..000000000 --- a/backend/src/models/insertables/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod dimensions; -pub mod global_config; -pub mod overrides; -pub mod contexts; -pub mod context_overrides; diff --git a/backend/src/models/insertables/overrides.rs b/backend/src/models/insertables/overrides.rs deleted file mode 100644 index cd5330ab7..000000000 --- a/backend/src/models/insertables/overrides.rs +++ /dev/null @@ -1,12 +0,0 @@ -use diesel::Insertable; -use serde::Serialize; -use serde_json::Value; - -use crate::db::schema::overrides; - -#[derive(Debug, Insertable, Serialize)] -#[diesel(table_name = overrides)] -pub struct NewOverride { - pub key : String, - pub value : Value, -} diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs deleted file mode 100644 index 215ab33bc..000000000 --- a/backend/src/models/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod db_models; -pub mod insertables; diff --git a/backend/src/utils/errors.rs b/backend/src/utils/errors.rs deleted file mode 100644 index d85318051..000000000 --- a/backend/src/utils/errors.rs +++ /dev/null @@ -1,78 +0,0 @@ -use serde::Serialize; -use actix_web::{error::ResponseError, http::StatusCode, HttpResponse, Either::{Left, Right}}; -use actix_web::Either; -use serde_json::{Value, to_value}; -use std::fmt; - -#[derive(Debug, Clone, Serialize)] -pub enum AppErrorType { - DataExists, - DBError, - NotFound, - SomethingWentWrong, - BadRequest -} - -#[derive(Debug)] -pub struct AppError { - pub message: Option, - pub cause: Option>, - pub status: AppErrorType, -} - -#[derive(Serialize)] -pub struct ErrorResponse { - status: AppErrorType, - message: String, - cause: Option, -} - -impl AppError { - fn message(&self) -> String { - match &*self { - AppError { message: Some(message), ..} => message.clone(), - _ => "Reason not available".to_string() - } - } - - fn cause(&self) -> Option { - match &*self { - AppError {cause, ..} => match cause { - Some(Left(val)) => to_value(val.clone()).ok(), - Some(Right(val)) => Some(val.clone()), - _ => None, - } - } - } -} - -impl fmt::Display for AppError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>{ - write!(f, "{:?}", self) - } -} - -impl ResponseError for AppError { - fn status_code(&self) -> StatusCode { - match self.status { - AppErrorType::SomethingWentWrong => StatusCode::INTERNAL_SERVER_ERROR, - AppErrorType::DBError => StatusCode::INTERNAL_SERVER_ERROR, - AppErrorType::NotFound => StatusCode::NOT_FOUND, - AppErrorType::BadRequest => StatusCode::BAD_REQUEST, - AppErrorType::DataExists => StatusCode::ALREADY_REPORTED, - } - } - - fn error_response(&self) -> HttpResponse { - let status_code = self.status_code(); - - let json_body = ErrorResponse - { message: self.message() - , cause: self.cause() - , status: self.status.clone() - }; - - HttpResponse::build(status_code).json(json_body) - } -} - diff --git a/backend/src/utils/hash.rs b/backend/src/utils/hash.rs deleted file mode 100644 index 2b41294cc..000000000 --- a/backend/src/utils/hash.rs +++ /dev/null @@ -1,11 +0,0 @@ - -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher} -}; - -pub fn string_based_b64_hash(obj: T) -> u64 where T: Hash, { - let mut hasher = DefaultHasher::new(); - obj.hash(&mut hasher); - hasher.finish() -} \ No newline at end of file diff --git a/backend/src/utils/helpers.rs b/backend/src/utils/helpers.rs deleted file mode 100644 index 480436ed7..000000000 --- a/backend/src/utils/helpers.rs +++ /dev/null @@ -1,43 +0,0 @@ -use serde_json::{Value, from_value, to_value}; -use std::{collections::BTreeMap}; - -pub fn sort_multi_level_keys_in_stringified_json(json: Value) -> Option { - let b_tree: &BTreeMap = &from_value(json).ok()?; - to_value(b_tree).ok() -} - -fn create_all_unique_subsets_helper(s: &Vec<&str> , idx: i32) -> Vec { - let mut n = idx; - let mut vector_index = 0; - let mut result: Vec = Vec::new(); - - while n > 0 { - if (n & 1) == 1 { - result.push(s[vector_index].to_owned()); - } - n = n >> 1; - vector_index += 1; - } - return result; -} - -pub fn create_all_unique_subsets(s: &Vec<&str>) -> Vec> { - let mut res: Vec> = Vec::new(); - for i in 1..(1 << s.len()) { - res.push(create_all_unique_subsets_helper(s ,i)); - } - return res; -} - - -pub fn split_stringified_key_value_pair(input: &str) -> Vec> { - let conditions_vector_splits: Vec<&str> = input.split("&").collect(); - let mut conditions_vector: Vec> = conditions_vector_splits - .iter() - .map(|&x| x.split("=").collect()) - .collect(); - - conditions_vector.sort_by(|a, b| a[0].cmp(&b[0])); - - return conditions_vector; -} \ No newline at end of file diff --git a/backend/src/utils/mod.rs b/backend/src/utils/mod.rs deleted file mode 100644 index f2991c6d9..000000000 --- a/backend/src/utils/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod hash; -pub mod helpers; -pub mod validations; -pub mod errors; diff --git a/backend/src/utils/validations.rs b/backend/src/utils/validations.rs deleted file mode 100644 index 2b64726a4..000000000 --- a/backend/src/utils/validations.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::collections::HashMap; - -use serde_json::{Value, to_value}; - -fn are_both_of_same_type(a: &Value, b: &Value) -> bool { - (a.is_boolean() && b.is_boolean()) - || (a.is_number() && b.is_number()) - || (a.is_string() && b.is_string()) - || (a.is_array() && b.is_array()) - || (a.is_object() && b.is_object()) -} - -fn type_of(a: &Value) -> String{ - if a.is_boolean() { - return "Boolean".to_string(); - } - - if a.is_number() { - return "Number".to_string(); - } - - if a.is_string() { - return "String".to_string(); - } - - if a.is_array() { - return "Array".to_string(); - } - - return "Object".to_string(); -} - -fn create_type_mismatch_error(default_value: &Value, overriding_value: &Value, path: &mut Vec) -> Value { - - let mut error_map = HashMap::new(); - error_map.insert("Path".to_string(), path.join("/")); - error_map.insert("Expected type".to_string(), type_of(default_value)); - error_map.insert("Encountered type".to_string(), type_of(overriding_value)); - - let error_message = to_value(error_map).unwrap(); // .map_err(|_| ValidationErrors::ErrorMessageParsingError)?; - - error_message -} - -fn create_structure_mismatch_error(path: &mut Vec) -> Value { - - let mut error_map = HashMap::new(); - - error_map.insert("Path".to_string(), path.join("/")); - if let Some(_) = path.last() { - error_map.insert("Reason".to_string(), "Key not found in default config".to_string()); - } - - let error_message = to_value(error_map).unwrap(); // .map_err(|_| ValidationErrors::ErrorMessageParsingError)?; - - error_message -} - -fn validate_sub_tree_helper(default_tree: &Value, overriding_tree: &Value, path: &mut Vec) -> Result { - - if !are_both_of_same_type(default_tree, overriding_tree) { - let error_message = create_type_mismatch_error(default_tree, overriding_tree, path); - return Err(error_message); - } - - if !default_tree.is_object() { - return Ok(true); - } - - let subtree = - overriding_tree.as_object().unwrap(); - - for (key, overriden_value) in (&*subtree).iter() { - - let default_value = default_tree.get(key); - - path.push((&key).to_string()); - - let result = match default_value { - Some(val) => validate_sub_tree_helper(val, overriden_value, path), - None => Err(create_structure_mismatch_error(path)) - }?; - - if !result { - return Ok(false); - } - } - - return Ok(true); -} - - -pub fn validate_sub_tree(default_tree: &Value, overriding_tree: &Value) -> Result { - validate_sub_tree_helper(default_tree, overriding_tree,&mut vec!["".to_string()]) -} - - -// pub fn just_for_test() { - -// let default_json: Value = from_str(r#" -// { "package_dependencies": { -// "in.juspay.dotp": { -// "entry": "base.html", -// "root": "payments/in.juspay.dotp/" -// }, -// "in.juspay.escrow": { -// "entry": "base.html", -// "root": "payments/in.juspay.escrow/", -// "hello": { -// "test": 123 -// } -// } -// } -// } -// "#).unwrap(); - -// let overridden_json: Value = from_str(r#" -// { "package_dependencies": { -// "in.juspay.escrow": { -// "entry": "base.html", -// "hello": { -// "test1": 123 -// } -// } -// } -// } -// "#).unwrap(); - -// let (ans, reason) = validate_sub_tree(&default_json, &overridden_json); -// println!("Result : {}", ans); -// println!("Reason : {}", reason); - -// } diff --git a/clients/haskell/cabal.project b/clients/haskell/cabal.project new file mode 100644 index 000000000..40050b05f --- /dev/null +++ b/clients/haskell/cabal.project @@ -0,0 +1,3 @@ +packages: + hs-cac-client + hs-exp-client diff --git a/clients/haskell/default.nix b/clients/haskell/default.nix new file mode 100644 index 000000000..1e18a8491 --- /dev/null +++ b/clients/haskell/default.nix @@ -0,0 +1,22 @@ +{ + perSystem = { config, pkgs, self', ... }: { + haskellProjects.default = { + projectRoot = ./.; + autoWire = [ "packages" "checks" "apps" ]; + settings = { + cac_client.custom = _: self'.packages.superposition; + experimentation_client.custom = _: self'.packages.superposition; + }; + }; + + devShells.haskell = pkgs.mkShell { + name = "superposition-haskell-clients"; + shellHook = '' + export LIBRARY_PATH=${self'.packages.superposition}/lib + ''; + inputsFrom = [ + config.haskellProjects.default.outputs.devShell + ]; + }; + }; +} diff --git a/clients/haskell/hs-cac-client/CHANGELOG.md b/clients/haskell/hs-cac-client/CHANGELOG.md new file mode 100644 index 000000000..9d0666ba0 --- /dev/null +++ b/clients/haskell/hs-cac-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/haskell/hs-cac-client/LICENSE b/clients/haskell/hs-cac-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/haskell/hs-cac-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/haskell/hs-cac-client/README.md b/clients/haskell/hs-cac-client/README.md new file mode 100644 index 000000000..1c2d58957 --- /dev/null +++ b/clients/haskell/hs-cac-client/README.md @@ -0,0 +1 @@ +# hs-cac-client diff --git a/clients/haskell/hs-cac-client/Setup.hs b/clients/haskell/hs-cac-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/haskell/hs-cac-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/haskell/hs-cac-client/hs-cac-client.cabal b/clients/haskell/hs-cac-client/hs-cac-client.cabal new file mode 100644 index 000000000..3edc1c64a --- /dev/null +++ b/clients/haskell/hs-cac-client/hs-cac-client.cabal @@ -0,0 +1,120 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-cac-client +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/githubuser/hs#readme +bug-reports: https://github.com/githubuser/hs/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs + +library + exposed-modules: + Client + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + extra-libraries: + cac_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exe + main-is: Main.hs + other-modules: + Paths_hs_cac_client + autogen-modules: + Paths_hs_cac_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + cac_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-cac-client + default-language: Haskell2010 \ No newline at end of file diff --git a/clients/haskell/hs-cac-client/src/Client.hs b/clients/haskell/hs-cac-client/src/Client.hs new file mode 100644 index 000000000..a5fe740bd --- /dev/null +++ b/clients/haskell/hs-cac-client/src/Client.hs @@ -0,0 +1,155 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( createCacClient +, getCacClient +, getFullConfigStateWithFilter +, getCacLastModified +, getResolvedConfig +, cacStartPolling +, getDefaultConfig +, getResolvedConfigWithStrategy +) where + +import Data.Aeson +import Data.Functor (($>)) +import Foreign.C.String (CString, newCAString, peekCAString) +import Foreign.C.Types (CInt (CInt), CULong (..)) +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Marshal.Array (withArrayLen) +import Data.List (intercalate) +import Foreign.Ptr +import Prelude + +data Arc_Client + +type CacClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_cac_client :: CTenant -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_cac_client :: FunPtr (Ptr CacClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_cac_client :: CTenant -> IO (Ptr CacClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "get_last_modified" + c_get_last_modified_time :: Ptr CacClient -> IO CString + +foreign import ccall unsafe "get_config" + c_get_config :: Ptr CacClient -> CString -> IO CString + +foreign import ccall unsafe "get_resolved_config" + c_cac_get_resolved_config :: Ptr CacClient -> CString -> CString -> CString -> IO CString + +foreign import ccall unsafe "get_default_config" + c_cac_get_default_config :: Ptr CacClient -> CString -> IO CString + +foreign import ccall safe "start_polling_update" + c_cac_poll :: CTenant -> IO () + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +data MergeStrategy = MERGE | REPLACE deriving (Show, Eq, Ord, Enum) + +cacStartPolling :: Tenant -> IO () +cacStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_cac_poll + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createCacClient:: Tenant -> Integer -> String -> IO (Either Error ()) +createCacClient tenant frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_cac_client cTenant duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) +getCacClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_cac_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_cac_client cacClient + +getFullConfigStateWithFilter :: ForeignPtr CacClient -> Maybe String -> IO (Either Error Value) +getFullConfigStateWithFilter client mbFilters = do + cFilters <- case mbFilters of + Just filters -> newCAString filters + Nothing -> return nullPtr + config <- withForeignPtr client (`c_get_config` cFilters) + _ <- cleanup [cFilters] + if config == nullPtr + then Left <$> getError + else do + fptrConfig <- newForeignPtr c_free_string config + Right . toJSON <$> withForeignPtr fptrConfig peekCAString + +getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) +getCacLastModified client = do + lastModified <- withForeignPtr client c_get_last_modified_time + if lastModified == nullPtr + then Left <$> getError + else do + fptrLastModified <- newForeignPtr c_free_string lastModified + Right <$> withForeignPtr fptrLastModified peekCAString + +getResolvedConfigWithStrategy :: ForeignPtr CacClient -> String -> Maybe [String] -> MergeStrategy -> IO (Either Error Value) +getResolvedConfigWithStrategy client context mbKeys mergeStrat = do + cContext <- newCAString context + cMergeStrat <- newCAString (show mergeStrat) + cStrKeys <- case mbKeys of + Just keys -> newCAString (intercalate "|" keys) + Nothing -> return nullPtr + overrides <- withForeignPtr client $ \client -> c_cac_get_resolved_config client cContext cStrKeys cMergeStrat + _ <- cleanup [cContext, cStrKeys] + if overrides == nullPtr + then Left <$> getError + else do + fptrOverrides <- newForeignPtr c_free_string overrides + Right . toJSON <$> withForeignPtr fptrOverrides peekCAString + +getDefaultConfig :: ForeignPtr CacClient -> Maybe [String] -> IO (Either Error Value) +getDefaultConfig client mbKeys = do + cStrKeys <- case mbKeys of + Just keys -> newCAString (intercalate "|" keys) + Nothing -> return nullPtr + overrides <- withForeignPtr client $ \client -> c_cac_get_default_config client cStrKeys + _ <- cleanup [cStrKeys] + if overrides == nullPtr + then Left <$> getError + else do + fptrOverrides <- newForeignPtr c_free_string overrides + Right . toJSON <$> withForeignPtr fptrOverrides peekCAString + +getResolvedConfig :: ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error Value) +getResolvedConfig client context mbKeys = getResolvedConfigWithStrategy client context mbKeys MERGE + diff --git a/clients/haskell/hs-cac-client/src/Main.hs b/clients/haskell/hs-cac-client/src/Main.hs new file mode 100644 index 000000000..9ad034b8a --- /dev/null +++ b/clients/haskell/hs-cac-client/src/Main.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (getResolvedConfig, createCacClient, getCacClient, + getFullConfigStateWithFilter, getCacLastModified, cacStartPolling, getDefaultConfig) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createCacClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkOS (cacStartPolling "dev") + print threadId + getCacClient "dev" >>= \case + Left err -> putStrLn err + Right client -> do + config <- getFullConfigStateWithFilter client Nothing + lastModified <- getCacLastModified client + overrides <- getResolvedConfig client "{\"country\": \"India\"}" $ Just ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client $ Just ["country_image_url", "hyperpay_version"] + filteredConfig <- getFullConfigStateWithFilter client $ Just "{\"prefix\": \"hyperpay\", \"os\": \"android\"}" + print config + print lastModified + print overrides + print defaults + print filteredConfig + threadDelay 1000000000 + pure () diff --git a/clients/haskell/hs-exp-client/CHANGELOG.md b/clients/haskell/hs-exp-client/CHANGELOG.md new file mode 100644 index 000000000..e43174087 --- /dev/null +++ b/clients/haskell/hs-exp-client/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `hs-exp-client` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/clients/haskell/hs-exp-client/LICENSE b/clients/haskell/hs-exp-client/LICENSE new file mode 100644 index 000000000..98e22917d --- /dev/null +++ b/clients/haskell/hs-exp-client/LICENSE @@ -0,0 +1,30 @@ +Copyright Author name here (c) 2024 + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Author name here nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/clients/haskell/hs-exp-client/README.md b/clients/haskell/hs-exp-client/README.md new file mode 100644 index 000000000..8401db649 --- /dev/null +++ b/clients/haskell/hs-exp-client/README.md @@ -0,0 +1 @@ +# hs-exp-client diff --git a/clients/haskell/hs-exp-client/Setup.hs b/clients/haskell/hs-exp-client/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/clients/haskell/hs-exp-client/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/clients/haskell/hs-exp-client/hs-exp-client.cabal b/clients/haskell/hs-exp-client/hs-exp-client.cabal new file mode 100644 index 000000000..6418689fe --- /dev/null +++ b/clients/haskell/hs-exp-client/hs-exp-client.cabal @@ -0,0 +1,121 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.36.0. +-- +-- see: https://github.com/sol/hpack + +name: hs-exp-client +version: 0.1.0.0 +description: Please see the README on GitHub at +homepage: https://github.com/githubuser/hs-exp-client#readme +bug-reports: https://github.com/githubuser/hs-exp-client/issues +author: Author name here +maintainer: example@example.com +copyright: 2024 Author name here +license: BSD-3-Clause +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +source-repository head + type: git + location: https://github.com/githubuser/hs-exp-client + +library + exposed-modules: + + Client + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded + extra-libraries: + experimentation_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + default-language: Haskell2010 + +executable hs-exp-client-exe + main-is: Main.hs + other-modules: + Paths_hs_exp_client + autogen-modules: + Paths_hs_exp_client + hs-source-dirs: + src + default-extensions: + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DuplicateRecordFields + ExplicitNamespaces + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedStrings + PatternSynonyms + PolyKinds + RankNTypes + RecordWildCards + ScopedTypeVariables + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + BlockArguments + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -threaded -rtsopts -with-rtsopts=-N + extra-libraries: + experimentation_client + build-depends: + aeson + , base >=4.7 && <5 + , basic-prelude + , hs-exp-client + default-language: Haskell2010 \ No newline at end of file diff --git a/clients/haskell/hs-exp-client/src/Client.hs b/clients/haskell/hs-exp-client/src/Client.hs new file mode 100644 index 000000000..7e28353e9 --- /dev/null +++ b/clients/haskell/hs-exp-client/src/Client.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +module Client +( expStartPolling +, getExpClient +, createExpClient +, getApplicableVariants +, getSatisfiedExperiments +, getRunningExperiments +) where + +import Data.Aeson.Types +import Data.Functor (($>)) +import Foreign (FunPtr, Ptr) +import Foreign.C (CInt (..), CShort (..), CULong (..)) +import Foreign.C.String +import Foreign.ForeignPtr +import Foreign.Marshal.Alloc (free) +import Foreign.Ptr (nullPtr) +import Prelude + +data Arc_Client + +type ExpClient = Arc_Client + +type CTenant = CString +type Tenant = String + +type Error = String + +foreign import ccall unsafe "new_client" + c_new_exp_client :: CTenant -> CULong -> CString -> IO CInt + +foreign import ccall unsafe "&free_client" + c_free_exp_client :: FunPtr (Ptr ExpClient -> IO ()) + +foreign import ccall unsafe "get_client" + c_get_exp_client :: CTenant -> IO (Ptr ExpClient) + +foreign import ccall unsafe "last_error_message" + c_last_error_message :: IO CString + +foreign import ccall unsafe "&free_string" + c_free_string :: FunPtr (CString -> IO ()) + +foreign import ccall unsafe "start_polling_update" + c_start_polling_update :: CTenant -> IO () + +foreign import ccall unsafe "get_applicable_variant" + c_get_applicable_variants :: Ptr ExpClient -> CString -> CShort -> IO CString + +foreign import ccall unsafe "get_satisfied_experiments" + c_get_satisfied_experiments :: Ptr ExpClient -> CString -> IO CString + +foreign import ccall unsafe "get_running_experiments" + c_get_running_experiments :: Ptr ExpClient -> IO CString + +expStartPolling :: Tenant -> IO () +expStartPolling tenant = + newCAString tenant + >>= newForeignPtr c_free_string + >>= flip withForeignPtr c_start_polling_update + +getError :: IO String +getError = c_last_error_message + >>= newForeignPtr c_free_string + >>= flip withForeignPtr peekCAString + +cleanup :: [Ptr a] -> IO () +cleanup items = mapM free items $> () + +createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) +createExpClient tenant frequency hostname = do + let duration = fromInteger frequency + cTenant <- newCAString tenant + cHostname <- newCAString hostname + resp <- c_new_exp_client cTenant duration cHostname + _ <- cleanup [cTenant, cHostname] + case resp of + 0 -> pure $ Right () + _ -> Left <$> getError + +getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) +getExpClient tenant = do + cTenant <- newCAString tenant + cacClient <- c_get_exp_client cTenant + _ <- cleanup [cTenant] + if cacClient == nullPtr + then Left <$> getError + else Right <$> newForeignPtr c_free_exp_client cacClient + +getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +getApplicableVariants client query toss = do + context <- newCAString query + variants <- withForeignPtr client (\c -> c_get_applicable_variants c context (fromInteger toss)) + _ <- cleanup [context] + if variants == nullPtr + then Left <$> getError + else do + fptrVariants <- newForeignPtr c_free_string variants + Right <$> withForeignPtr fptrVariants peekCAString + -- pure $ + -- case fromJSON variantVector of + -- Error s -> Left s + -- Success vec -> Right vec + +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +getSatisfiedExperiments client query = do + context <- newCAString query + experiments <- withForeignPtr client (`c_get_satisfied_experiments` context) + _ <- cleanup [context] + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString + +getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) +getRunningExperiments client = do + experiments <- withForeignPtr client c_get_running_experiments + if experiments == nullPtr + then Left <$> getError + else do + fptrExperiments <- newForeignPtr c_free_string experiments + Right . toJSON <$> withForeignPtr fptrExperiments peekCAString diff --git a/clients/haskell/hs-exp-client/src/Main.hs b/clients/haskell/hs-exp-client/src/Main.hs new file mode 100644 index 000000000..bb7621266 --- /dev/null +++ b/clients/haskell/hs-exp-client/src/Main.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (createExpClient, expStartPolling, + getApplicableVariants, getExpClient, + getRunningExperiments, + getSatisfiedExperiments) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createExpClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkIO (expStartPolling "dev") + print threadId + getExpClient "dev" >>= \case + Left err -> putStrLn err + Right client -> loop client + pure () + where + loop client = do + runningExperiments <- getRunningExperiments client + satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" + variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + print "Running experiments" + print runningExperiments + print "experiments that satisfy context" + print satisfiedExperiments + print "variant ID applied" + print variants + -- threadDelay 10000000 + loop client diff --git a/clients/js/index.js b/clients/js/index.js new file mode 100644 index 000000000..696ab0765 --- /dev/null +++ b/clients/js/index.js @@ -0,0 +1 @@ +!function(r,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["Context-Aware-Config"]=t():r["Context-Aware-Config"]=t()}(self,(function(){return r={193:function(r,t,e){"use strict";var n=this&&this.__assign||function(){return n=Object.assign||function(r){for(var t,e=1,n=arguments.length;e>",c.compareSemanticIsGreater)},925:function(r,t){"use strict";var e=this&&this.__spreadArray||function(r,t,e){if(e||2===arguments.length)for(var n,i=0,o=t.length;in[i])return!0;return!1};t.compareSemanticIsGreater=function(r,t,n){return void 0===n?e(r,t):e(r,t)&&e(t,n)}},962:function(r,t,e){var n,i;n=function(){"use strict";Array.isArray||(Array.isArray=function(r){return"[object Array]"===Object.prototype.toString.call(r)});var r={},t={"==":function(r,t){return r==t},"===":function(r,t){return r===t},"!=":function(r,t){return r!=t},"!==":function(r,t){return r!==t},">":function(r,t){return r>t},">=":function(r,t){return r>=t},"<":function(r,t,e){return void 0===e?r=t?[]:n}};return r.is_logic=function(r){return"object"==typeof r&&null!==r&&!Array.isArray(r)&&1===Object.keys(r).length},r.truthy=function(r){return!(Array.isArray(r)&&0===r.length||!r)},r.get_operator=function(r){return Object.keys(r)[0]},r.get_values=function(t){return t[r.get_operator(t)]},r.apply=function(e,n){if(Array.isArray(e))return e.map((function(t){return r.apply(t,n)}));if(!r.is_logic(e))return e;var i,o,u,a,f,l=r.get_operator(e),c=e[l];if(Array.isArray(c)||(c=[c]),"if"===l||"?:"==l){for(i=0;i0){var p=String(l).split("."),s=t;for(i=0;i=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.19", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/json-logic-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", + "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + } + }, + "node_modules/babel-core/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/babel-core/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-core/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "dependencies": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "node_modules/babel-generator/node_modules/jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha512-TYX2QQATKA6Wssp6j7jqlw4QLmABDN1olRdEHndYvBXdaXM5dcx6j5rN0+nd+aVL+Th40fAEYvvw/Xxd/LETuQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==", + "dev": true, + "dependencies": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==", + "dev": true, + "dependencies": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha512-SFbWewr0/0U4AiRzsHqwsbOQeLXVa9T1ELdqEa2efcQB5KopTnunAqoj07TuHlN2lfTQNPGO/rJR4FMln5fVcA==", + "dev": true, + "dependencies": { + "babel-helper-bindify-decorators": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==", + "dev": true, + "dependencies": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==", + "dev": true, + "dependencies": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha512-EbciFN5Jb9iqU9bqaLmmFLx2G8pAUsvpWJ6OzOWBNrSY9qTohXj+7YfZx6Ug1Qqh7tCb1EA7Jvn9bMC1HBiucg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha512-EEuBcXz/wZ81Jaac0LnMHtD4Mfz9XWn2oH2Xj+CHwz2SZWUqqdtR2BgWPSdTGMmxN/5KLSh4PImt9+9ZedDarA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha512-AWj19x2aDm8qFQ5O2JcD6pwJDW1YdcnO+1b81t7gxrGjz5VHiUqeYWAR4h7zueWMalRelrQDXprv2FrY1dbpbw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha512-HD/5qJB9oSXzl0caxM+aRD7ENICXqcc3Up/8toDQk7zNIDE7TzsqtxC5f4t9Rwhu2Ya8l9l4j6b3vOsy+a6qxg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==", + "dev": true + }, + "node_modules/babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==", + "dev": true + }, + "node_modules/babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg==", + "dev": true + }, + "node_modules/babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha512-m8yMoh9LIiNyeLdQs5I9G+3YXo4nqVsKQkk7YplrG4qAFbNi9hkZlow8HDHxhH9QOVFPHmy8+03NzRCdyChIKw==", + "dev": true + }, + "node_modules/babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "node_modules/babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==", + "dev": true + }, + "node_modules/babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==", + "dev": true, + "dependencies": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-generators": "^6.5.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==", + "dev": true, + "dependencies": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha512-RvYukT1Nh7njz8P8326ztpQUGCKwmjgu6aRIx1lkvylWITYcskg29vy1Kp8WXIq7FvhXsz0Crf2kS94bjB690A==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-class-constructor-call": "^6.18.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha512-skQ2CImwDkCHu0mkWvCOlBCpBIHW4/49IZWVwV4A/EnWjL9bB6UBvLyMNe3Td5XDStSZNhe69j4bfEW8dvUbew==", + "dev": true, + "dependencies": { + "babel-helper-explode-class": "^6.24.1", + "babel-plugin-syntax-decorators": "^6.13.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha512-yQwYqYg+Tnj1InA8W1rsItsZVhkv1Euc4KVua9ledtPz5PDWYz7LVyy6rDBpVYUWFZj5k6GUm3YZpCbIm8Tqew==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-do-expressions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==", + "dev": true, + "dependencies": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==", + "dev": true, + "dependencies": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==", + "dev": true, + "dependencies": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "dependencies": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==", + "dev": true, + "dependencies": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==", + "dev": true, + "dependencies": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==", + "dev": true, + "dependencies": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==", + "dev": true, + "dependencies": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==", + "dev": true, + "dependencies": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==", + "dev": true, + "dependencies": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "node_modules/babel-plugin-transform-es2015-unicode-regex/node_modules/regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==", + "dev": true, + "dependencies": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-export-extensions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha512-9Ec4KYf1GurT39mlUjDSlN7HWSlB3u3mWRMogQbb+Y88lO0ZM3rJ0ADhPnQwWK9TbO6e/4E+Et1rrfGY9mFimA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-function-bind": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==", + "dev": true, + "dependencies": { + "regenerator-transform": "^0.10.0" + } + }, + "node_modules/babel-plugin-transform-regenerator/node_modules/regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "node_modules/babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + } + }, + "node_modules/babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha512-XfwUqG1Ry6R43m4Wfob+vHbIVBIqTg/TJY4Snku1iIzeH7mUnwHA8Vagmv+ZQbPwhS8HgsdQvy28Py3k5zpoFQ==", + "deprecated": "🙌 Thanks for using Babel: we recommend using babel-preset-env now: please read https://babeljs.io/env to update!", + "dev": true, + "dependencies": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==", + "dev": true, + "dependencies": { + "babel-plugin-transform-do-expressions": "^6.22.0", + "babel-plugin-transform-function-bind": "^6.22.0", + "babel-preset-stage-1": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha512-rn+UOcd7BHDniq1SVxv2/AVVSVI1NK+hfS0I/iR6m6KbOi/aeBRcqBilqO73pd9VUpRXF2HFtlDuC9F2BEQqmg==", + "dev": true, + "dependencies": { + "babel-plugin-transform-class-constructor-call": "^6.24.1", + "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-preset-stage-2": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha512-9F+nquz+37PrlTSBdpeQBKnQfAMNBnryXw+m4qBh35FNbJPfzZz+sjN2G5Uf1CRedU9PH7fJkTbYijxmkLX8Og==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-preset-stage-3": "^6.24.1" + } + }, + "node_modules/babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha512-eCbEOF8uN0KypFXJmZXn2sTk7bPV9uM5xov7G/7BM08TbQEObsVs0cEWfy6NQySlfk7JBi/t+XJP1JkruYfthA==", + "dev": true, + "dependencies": { + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-exponentiation-operator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.22.0" + } + }, + "node_modules/babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==", + "dev": true, + "dependencies": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/babel-traverse/node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-traverse/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babel-types/node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.11", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz", + "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001538", + "electron-to-chromium": "^1.4.526", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001538", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", + "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.527", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.527.tgz", + "integrity": "sha512-EafxEiEDzk2aLrdbtVczylHflHdHkNrpGNHIgDyA63sUQLQVS2ayj2hPw3RsVB42qkwURH+T2OxV7kGPUuYszA==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "dependencies": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "dev": true + }, + "@babel/core": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.22.20", + "@babel/helpers": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", + "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", + "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "requires": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.15.tgz", + "integrity": "sha512-qLNsZbgrNh0fDQBCPocSL8guki1hcPvltGDv/NxvUoABwFq7GkKSu1nRXeJkVZc+wJvne2E0RKQz+2SQrz6eAA==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + } + }, + "@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + } + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + } + }, + "@babel/helpers": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", + "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.15" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "requires": {} + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", + "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.15.tgz", + "integrity": "sha512-G1czpdJBZCtngoK1sJgloLiOHUnkb/bLZwqVZD8kXmq0ZnVfTTWUcs9OWtp0mBtYJ+4LQY1fllqBkOIPhXmFmw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", + "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.15.tgz", + "integrity": "sha512-HzG8sFl1ZVGTme74Nw+X01XsUTqERVQ6/RLHo3XjGRzm7XD6QTtfS3NJotVgCGy8BzkDqRjRBD8dAyJn5TuvSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", + "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", + "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", + "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", + "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.15.tgz", + "integrity": "sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.11.tgz", + "integrity": "sha512-rIqHmHoMEOhI3VkVf5jQ15l539KrwhzqcBO6wdCNWPWc/JWt9ILNYNUssbRpeq0qWns8svuw8LnMNCvWBIJ8wA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", + "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", + "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", + "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.15" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", + "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.15.tgz", + "integrity": "sha512-ngQ2tBhq5vvSJw2Q2Z9i7ealNkpDMU0rGWnHPKqRZO0tzZ5tlaoz4hDvhXioOoaE0X2vfNss1djwg0DXlfu30A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.22.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", + "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", + "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.22.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", + "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/preset-env": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.20", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.15", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.15", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.15", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.15", + "@babel/plugin-transform-modules-systemjs": "^7.22.11", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.22.15", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "@babel/types": "^7.22.19", + "babel-plugin-polyfill-corejs2": "^0.4.5", + "babel-plugin-polyfill-corejs3": "^0.8.3", + "babel-plugin-polyfill-regenerator": "^0.5.2", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + } + }, + "@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", + "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.19", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.22.19", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", + "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.19", + "to-fast-properties": "^2.0.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", + "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/eslint": { + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "@types/json-logic-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", + "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "dev": true + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "requires": {} + }, + "@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "requires": {} + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", + "dev": true + } + } + }, + "babel-helper-bindify-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", + "integrity": "sha512-TYX2QQATKA6Wssp6j7jqlw4QLmABDN1olRdEHndYvBXdaXM5dcx6j5rN0+nd+aVL+Th40fAEYvvw/Xxd/LETuQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-explode-class": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", + "integrity": "sha512-SFbWewr0/0U4AiRzsHqwsbOQeLXVa9T1ELdqEa2efcQB5KopTnunAqoj07TuHlN2lfTQNPGO/rJR4FMln5fVcA==", + "dev": true, + "requires": { + "babel-helper-bindify-decorators": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "requires": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", + "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.2", + "semver": "^6.3.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz", + "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.2", + "core-js-compat": "^3.31.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", + "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.4.2" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==", + "dev": true + }, + "babel-plugin-syntax-async-generators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", + "integrity": "sha512-EbciFN5Jb9iqU9bqaLmmFLx2G8pAUsvpWJ6OzOWBNrSY9qTohXj+7YfZx6Ug1Qqh7tCb1EA7Jvn9bMC1HBiucg==", + "dev": true + }, + "babel-plugin-syntax-class-constructor-call": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", + "integrity": "sha512-EEuBcXz/wZ81Jaac0LnMHtD4Mfz9XWn2oH2Xj+CHwz2SZWUqqdtR2BgWPSdTGMmxN/5KLSh4PImt9+9ZedDarA==", + "dev": true + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==", + "dev": true + }, + "babel-plugin-syntax-decorators": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", + "integrity": "sha512-AWj19x2aDm8qFQ5O2JcD6pwJDW1YdcnO+1b81t7gxrGjz5VHiUqeYWAR4h7zueWMalRelrQDXprv2FrY1dbpbw==", + "dev": true + }, + "babel-plugin-syntax-do-expressions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", + "integrity": "sha512-HD/5qJB9oSXzl0caxM+aRD7ENICXqcc3Up/8toDQk7zNIDE7TzsqtxC5f4t9Rwhu2Ya8l9l4j6b3vOsy+a6qxg==", + "dev": true + }, + "babel-plugin-syntax-dynamic-import": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", + "integrity": "sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==", + "dev": true + }, + "babel-plugin-syntax-export-extensions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", + "integrity": "sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg==", + "dev": true + }, + "babel-plugin-syntax-function-bind": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", + "integrity": "sha512-m8yMoh9LIiNyeLdQs5I9G+3YXo4nqVsKQkk7YplrG4qAFbNi9hkZlow8HDHxhH9QOVFPHmy8+03NzRCdyChIKw==", + "dev": true + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==", + "dev": true + }, + "babel-plugin-transform-async-generator-functions": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", + "integrity": "sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-generators": "^6.5.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "^6.24.1", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-class-constructor-call": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", + "integrity": "sha512-RvYukT1Nh7njz8P8326ztpQUGCKwmjgu6aRIx1lkvylWITYcskg29vy1Kp8WXIq7FvhXsz0Crf2kS94bjB690A==", + "dev": true, + "requires": { + "babel-plugin-syntax-class-constructor-call": "^6.18.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-decorators": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", + "integrity": "sha512-skQ2CImwDkCHu0mkWvCOlBCpBIHW4/49IZWVwV4A/EnWjL9bB6UBvLyMNe3Td5XDStSZNhe69j4bfEW8dvUbew==", + "dev": true, + "requires": { + "babel-helper-explode-class": "^6.24.1", + "babel-plugin-syntax-decorators": "^6.13.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-do-expressions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", + "integrity": "sha512-yQwYqYg+Tnj1InA8W1rsItsZVhkv1Euc4KVua9ledtPz5PDWYz7LVyy6rDBpVYUWFZj5k6GUm3YZpCbIm8Tqew==", + "dev": true, + "requires": { + "babel-plugin-syntax-do-expressions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + } + } + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", + "babel-plugin-syntax-exponentiation-operator": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-export-extensions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", + "integrity": "sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg==", + "dev": true, + "requires": { + "babel-plugin-syntax-export-extensions": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-function-bind": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", + "integrity": "sha512-9Ec4KYf1GurT39mlUjDSlN7HWSlB3u3mWRMogQbb+Y88lO0ZM3rJ0ADhPnQwWK9TbO6e/4E+Et1rrfGY9mFimA==", + "dev": true, + "requires": { + "babel-plugin-syntax-function-bind": "^6.8.0", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==", + "dev": true, + "requires": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + }, + "dependencies": { + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + } + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha512-XfwUqG1Ry6R43m4Wfob+vHbIVBIqTg/TJY4Snku1iIzeH7mUnwHA8Vagmv+ZQbPwhS8HgsdQvy28Py3k5zpoFQ==", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-preset-stage-0": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", + "integrity": "sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==", + "dev": true, + "requires": { + "babel-plugin-transform-do-expressions": "^6.22.0", + "babel-plugin-transform-function-bind": "^6.22.0", + "babel-preset-stage-1": "^6.24.1" + } + }, + "babel-preset-stage-1": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", + "integrity": "sha512-rn+UOcd7BHDniq1SVxv2/AVVSVI1NK+hfS0I/iR6m6KbOi/aeBRcqBilqO73pd9VUpRXF2HFtlDuC9F2BEQqmg==", + "dev": true, + "requires": { + "babel-plugin-transform-class-constructor-call": "^6.24.1", + "babel-plugin-transform-export-extensions": "^6.22.0", + "babel-preset-stage-2": "^6.24.1" + } + }, + "babel-preset-stage-2": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", + "integrity": "sha512-9F+nquz+37PrlTSBdpeQBKnQfAMNBnryXw+m4qBh35FNbJPfzZz+sjN2G5Uf1CRedU9PH7fJkTbYijxmkLX8Og==", + "dev": true, + "requires": { + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators": "^6.24.1", + "babel-preset-stage-3": "^6.24.1" + } + }, + "babel-preset-stage-3": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", + "integrity": "sha512-eCbEOF8uN0KypFXJmZXn2sTk7bPV9uM5xov7G/7BM08TbQEObsVs0cEWfy6NQySlfk7JBi/t+XJP1JkruYfthA==", + "dev": true, + "requires": { + "babel-plugin-syntax-trailing-function-commas": "^6.22.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", + "babel-plugin-transform-async-to-generator": "^6.24.1", + "babel-plugin-transform-exponentiation-operator": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.22.0" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.11", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.11.tgz", + "integrity": "sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001538", + "electron-to-chromium": "^1.4.526", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001538", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz", + "integrity": "sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.32.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.2.tgz", + "integrity": "sha512-+GjlguTDINOijtVRUxrQOv3kfu9rl+qPNdX2LTbJ/ZyVTuxK+ksVSAGX1nHstu4hrv1En/uPTtWgq2gI5wt4AQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.10" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "electron-to-chromium": { + "version": "1.4.527", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.527.tgz", + "integrity": "sha512-EafxEiEDzk2aLrdbtVczylHflHdHkNrpGNHIgDyA63sUQLQVS2ayj2hPw3RsVB42qkwURH+T2OxV7kGPUuYszA==", + "dev": true + }, + "enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true + }, + "es-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "requires": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "dev": true, + "requires": { + "flatted": "^3.2.7", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-logic-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", + "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "keyv": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "requires": { + "find-up": "^6.3.0" + }, + "dependencies": { + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true + } + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==", + "dev": true + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "resolve": { + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "terser": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.20.0.tgz", + "integrity": "sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", + "dev": true + }, + "ts-loader": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", + "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "webpack": { + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + } + } + }, + "webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/clients/js/package.json b/clients/js/package.json new file mode 100644 index 000000000..caa279b00 --- /dev/null +++ b/clients/js/package.json @@ -0,0 +1,37 @@ +{ + "name": "Cac-Client", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "test": "node ./tests/index.js", + "compile": "npx tsc", + "dev": "tsc --project tsconfig.json", + "buildLib": "webpack --mode=production", + "build": "run-s secure dev buildLib", + "start": "nodemon ./dist/app.js", + "lint": "eslint . --ext .ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "json-logic-js": "^2.0.2" + }, + "devDependencies": { + "@babel/core": "^7.22.10", + "@babel/preset-env": "^7.22.10", + "@types/json-logic-js": "^2.0.1", + "@types/node": "^18.11.9", + "babel-core": "^6.26.3", + "babel-loader": "^9.1.3", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-0": "^6.24.1", + "eslint": "^8.47.0", + "ts-loader": "^9.4.4", + "typescript": "^4.9.5", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + } + } diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts new file mode 100644 index 000000000..7b1c1ab18 --- /dev/null +++ b/clients/js/src/index.ts @@ -0,0 +1,41 @@ +import * as jsonLogic from 'json-logic-js'; +import { deepMerge } from './utils/deepMerge'; +import { compareSemanticIsGreater } from './utils/operations' +import { IObject, Dimension } from './types' + +type DataFromCacApi = { + contexts: Array; + overrides: IObject; + default_configs: IObject; +} + +export class CacReader { + contexts: Array; + overrides: IObject; + defaultConfig: IObject; + + static { + jsonLogic.add_operation(">>", compareSemanticIsGreater); + } + + constructor(completeConfig: DataFromCacApi) { + this.contexts = completeConfig.contexts; + this.overrides = completeConfig.overrides; + this.defaultConfig = completeConfig.default_configs; + } + + public evaluateConfig(data: IObject): IObject { + + const requiredOverrides: Array = []; + for (let i = 0; i < this.contexts.length; i++) { + if (jsonLogic.apply(this.contexts[i].condition, data)) { + requiredOverrides.push( + ...this.contexts[i].override_with_keys.map(x => this.overrides[x]) + ); + } + } + + const targetConfig: IObject = { ...this.defaultConfig }; + return deepMerge(targetConfig, ...requiredOverrides); + } +} diff --git a/clients/js/src/types.ts b/clients/js/src/types.ts new file mode 100644 index 000000000..e9a023701 --- /dev/null +++ b/clients/js/src/types.ts @@ -0,0 +1,16 @@ +export type IObject = { + [key: string]: any; +} + +export type Dimension = { + condition: IObject, + override_with_keys: Array +} + +export type DimensionConfig = { + dimensions: Array +}; + +export type IIsObject = { + (item: any): boolean; +} \ No newline at end of file diff --git a/clients/js/src/utils/deepMerge.ts b/clients/js/src/utils/deepMerge.ts new file mode 100644 index 000000000..456813bd8 --- /dev/null +++ b/clients/js/src/utils/deepMerge.ts @@ -0,0 +1,44 @@ +import { IIsObject, IObject } from '../types' + + +const isObject: IIsObject = (item: any): boolean => { + return (item === Object(item) && !Array.isArray(item)); +}; + +export const deepMerge = (target: IObject, ...sources: Array): IObject => { + // return the target if no sources passed + if (!sources.length) { + return target; + } + + const result: IObject = target; + + if (isObject(result)) { + const len: number = sources.length; + + for (let i = 0; i < len; i += 1) { + const elm: any = sources[i]; + + if (isObject(elm)) { + for (const key in elm) { + if (elm.hasOwnProperty(key)) { + if (isObject(elm[key])) { + if (!result[key] || !isObject(result[key])) { + result[key] = {}; + } + deepMerge(result[key], elm[key]); + } else { + if (Array.isArray(result[key]) && Array.isArray(elm[key])) { + result[key] = [...elm[key]]; + } else { + result[key] = elm[key]; + } + } + } + } + } + } + } + + return result; +}; diff --git a/clients/js/src/utils/operations.ts b/clients/js/src/utils/operations.ts new file mode 100644 index 000000000..9484e5e2b --- /dev/null +++ b/clients/js/src/utils/operations.ts @@ -0,0 +1,9 @@ +const compareSemanticIsGreaterImp = function (version_a: string, version_b: string) { + return version_a.localeCompare(version_b, undefined, {numeric:true, sensitivity:'base'}) > 0; +} + +export const compareSemanticIsGreater = function (version_a: string, version_b: string, version_c?: string) { + return (version_c === undefined ? + compareSemanticIsGreaterImp(version_a, version_b) : + (compareSemanticIsGreaterImp(version_a, version_b) && compareSemanticIsGreaterImp(version_b, version_c))); +} diff --git a/clients/js/tsconfig.json b/clients/js/tsconfig.json new file mode 100644 index 000000000..d2bb6eebc --- /dev/null +++ b/clients/js/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "lib": ["es6"], + "allowJs": false, + "outDir": "./dist", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["./src/**/*"], + "exclude": ["node_modules"] + } diff --git a/clients/js/webpack.config.js b/clients/js/webpack.config.js new file mode 100644 index 000000000..16e818cb6 --- /dev/null +++ b/clients/js/webpack.config.js @@ -0,0 +1,36 @@ +const path = require('path'); + +module.exports = { + entry: './dist/index.js', + output: { + path: path.resolve(__dirname), + filename: "index.js", + library : { + name : "Context-Aware-Config", + type: "umd" + }, + environment: { + arrowFunction: false, + bigIntLiteral: false, + const: true, + destructuring: false, + dynamicImport: false, + forOf: true, + module: true + } + }, + module: { + rules: [ + { test: /\.js$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env"], + } + } + }, + ] + } +}; + diff --git a/cog.toml b/cog.toml new file mode 100644 index 000000000..758a5cacd --- /dev/null +++ b/cog.toml @@ -0,0 +1,36 @@ +from_latest_tag = true +ignore_merge_commits = false +branch_whitelist = [] + +tag_prefix = "v" +monorepo_version_separator = "-" + +pre_bump_hooks = [] + +pre_package_bump_hooks = [ + "echo 'upgrading {{package}}' to {{version}}", + "cargo set-version --package {{package}} {{version}}" +] + +post_package_bump_hooks = [] + +post_bump_hooks = [] + +[commit_types] + +[changelog] +path = "CHANGELOG.md" +authors = [] + +[bump_profiles] + +[packages] +context_aware_config = { path = "crates/context_aware_config" } +experimentation_platform = { path = "crates/experimentation_platform" } +service_utils = { path = "crates/service_utils" } +external = { path = "crates/external" } +frontend = { path = "crates/frontend" } +caclang = { path = "crates/caclang" } +cac_client = { path = "crates/cac_client" } +experimentation_client = { path = "crates/experimentation_client" } +superposition_types = { path = "crates/superposition_types" } \ No newline at end of file diff --git a/crates/cac_client/CHANGELOG.md b/crates/cac_client/CHANGELOG.md new file mode 100644 index 000000000..0d3d80e74 --- /dev/null +++ b/crates/cac_client/CHANGELOG.md @@ -0,0 +1,91 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## cac_client-v0.9.1 - 2024-04-17 +#### Bug Fixes +- [PICAF-26653] removed audit log middleware and reduced max db connection pool size to 2 - (82022eb) - Saurav Suman + +- - - + +## cac_client-v0.9.0 - 2024-04-16 +#### Bug Fixes +- PICAF-26366 fixed error in client - (d1b1f03) - ankit.mahato +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + +## cac_client-v0.8.0 - 2024-04-05 +#### Bug Fixes +- [PICAF-26101] empty key filters should return all keys - (f9dd889) - Kartik +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26101] client interface improvements - (d606cb1) - Kartik +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik + +- - - + +## cac_client-v0.7.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + +## cac_client-v0.6.0 - 2024-03-04 +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + +## cac_client-v0.5.1 - 2024-02-22 +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in + +- - - + +## cac_client-v0.5.0 - 2024-01-04 +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra + +- - - + +## cac_client-v0.4.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## cac_client-v0.3.0 - 2023-10-27 +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + +## cac_client-v0.2.1 - 2023-09-20 +#### Bug Fixes +- PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan + +- - - + +## cac_client-v0.2.0 - 2023-09-12 +#### Bug Fixes +- PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra +#### Features +- PICAF-24223 cac eval return update - (d558ddc) - Pratik Mishra +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra + +- - - + +## cac_client-v0.1.0 - 2023-09-01 +#### Features +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/cac_client/Cargo.toml b/crates/cac_client/Cargo.toml new file mode 100644 index 000000000..198772578 --- /dev/null +++ b/crates/cac_client/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "cac_client" +version = "0.9.1" +edition = "2021" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = { workspace = true } +derive_more = { workspace = true } +actix-web = { workspace = true } +chrono = { workspace = true } +jsonlogic = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +log = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } +tokio = {version = "1.29.1", features = ["full"]} +service_utils = { path = "../service_utils" } +anyhow = { workspace = true } +[lib] +name = "cac_client" +crate-type = ["cdylib", "lib"] + +[build-dependencies] +cbindgen = "0.26.0" diff --git a/crates/cac_client/build.rs b/crates/cac_client/build.rs new file mode 100644 index 000000000..cb9d3c4d2 --- /dev/null +++ b/crates/cac_client/build.rs @@ -0,0 +1,11 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + println!("Calling build.rs in cac_client"); + cbindgen::generate_with_config(&crate_dir, config) + .unwrap() + .write_to_file("../../headers/libcac_client.h"); +} diff --git a/crates/cac_client/src/eval.rs b/crates/cac_client/src/eval.rs new file mode 100644 index 000000000..fe3795dba --- /dev/null +++ b/crates/cac_client/src/eval.rs @@ -0,0 +1,155 @@ +//NOTE this code is copied over from sdk-config-server with small changes for compatiblity +//TODO refactor, make eval MJOS agnostic + +use crate::{utils::core::MapError, Context, MergeStrategy}; +use jsonlogic; +use serde_json::{json, Map, Value}; + +pub fn merge(doc: &mut Value, patch: &Value) { + if !patch.is_object() { + *doc = patch.clone(); + return; + } + + if !doc.is_object() { + *doc = Value::Object(Map::new()); + } + let map = doc.as_object_mut().unwrap(); + for (key, value) in patch.as_object().unwrap() { + merge(map.entry(key.as_str()).or_insert(Value::Null), value); + } +} + +fn replace_top_level( + doc: &mut Map, + patch: &Value, + mut on_override: impl FnMut(), + override_key: &String, +) { + match patch.as_object() { + Some(patch_map) => { + for (key, value) in patch_map { + doc.insert(key.clone(), value.clone()); + } + on_override(); + } + None => { + log::error!("CAC: found non-object override key: {override_key} in overrides") + } + } +} + +fn get_overrides( + query_data: &Map, + contexts: &Vec, + overrides: &Map, + merge_strategy: &MergeStrategy, + mut on_override_select: Option<&mut dyn FnMut(Context)>, +) -> serde_json::Result { + let mut required_overrides: Value = json!({}); + let mut on_override_select = |context: Context| match on_override_select { + Some(ref mut func) => func(context), + None => (), + }; + + for context in contexts.iter() { + // TODO :: Add semantic version comparator in Lib + if let Ok(Value::Bool(true)) = + jsonlogic::apply(&context.condition, &json!(query_data)) + { + for override_key in &context.override_with_keys { + if let Some(overriden_value) = overrides.get(override_key) { + match merge_strategy { + MergeStrategy::REPLACE => replace_top_level( + &mut required_overrides.as_object_mut().unwrap(), + overriden_value, + || on_override_select(context.clone()), + override_key, + ), + MergeStrategy::MERGE => { + merge(&mut required_overrides, overriden_value); + on_override_select(context.clone()) + } + } + } + } + } + } + + Ok(required_overrides) +} + +fn merge_overrides_on_default_config( + default_config: &mut Map, + overrides: Map, + merge_strategy: &MergeStrategy, +) { + overrides.into_iter().for_each(|(key, val)| { + if let Some(og_val) = default_config.get_mut(&key) { + match merge_strategy { + MergeStrategy::REPLACE => { + let _ = default_config.insert(key.clone(), val.clone()); + } + MergeStrategy::MERGE => merge(og_val, &val), + } + } else { + log::error!("CAC: found non-default_config key: {key} in overrides"); + } + }) +} + +pub fn eval_cac( + mut default_config: Map, + contexts: &Vec, + overrides: &Map, + query_data: &Map, + merge_strategy: MergeStrategy, +) -> Result, String> { + let on_override_select: Option<&mut dyn FnMut(Context)> = None; + let overrides: Map = get_overrides( + &query_data, + &contexts, + &overrides, + &merge_strategy, + on_override_select, + ) + .and_then(serde_json::from_value) + .map_err_to_string()?; + merge_overrides_on_default_config(&mut default_config, overrides, &merge_strategy); + let overriden_config = default_config; + Ok(overriden_config) +} + +pub fn eval_cac_with_reasoning( + mut default_config: Map, + contexts: &Vec, + overrides: &Map, + query_data: &Map, + merge_strategy: MergeStrategy, +) -> Result, String> { + let mut reasoning: Vec = vec![]; + + let applied_overrides: Map = get_overrides( + &query_data, + &contexts, + &overrides, + &merge_strategy, + Some(&mut |context| { + reasoning.push(json!({ + "context": context.condition, + "override": context.override_with_keys + })) + }), + ) + .and_then(serde_json::from_value) + .map_err_to_string()?; + + merge_overrides_on_default_config( + &mut default_config, + applied_overrides, + &merge_strategy, + ); + let mut overriden_config = default_config; + overriden_config.insert("metadata".into(), json!(reasoning)); + Ok(overriden_config) +} diff --git a/crates/cac_client/src/interface.rs b/crates/cac_client/src/interface.rs new file mode 100644 index 000000000..55d2efd59 --- /dev/null +++ b/crates/cac_client/src/interface.rs @@ -0,0 +1,307 @@ +// Primary interface so CAC client can work with other languages like haskell +#[warn(unused_assignments)] +use std::{ + ffi::{c_char, c_ulong, CStr}, + sync::Arc, +}; + +use crate::{utils::core::MapError, Client, MergeStrategy, CLIENT_FACTORY}; +use serde_json::{Map, Value}; +use std::{ + cell::RefCell, + ffi::{c_int, CString}, + time::Duration, +}; +use tokio::{runtime::Runtime, task}; + +thread_local! { + static LAST_ERROR: RefCell>> = RefCell::new(None); +} + +macro_rules! null_check { + ($client: ident, $err: literal, $return: stmt) => { + if $client.is_null() { + update_last_error($err.into()); + $return + } + }; +} + +macro_rules! unwrap_safe { + ($result: expr, $return: stmt) => { + match $result { + Ok(value) => value, + Err(err) => { + update_last_error(err.to_string()); + $return + } + } + }; +} + +fn cstring_to_rstring(s: *const c_char) -> Result { + null_check!( + s, + "Invalid C string passed: string was a NULL pointer", + return Err("Invalid C string passed: string was a NULL pointer".into()) + ); + let s = unsafe { CStr::from_ptr(s) }; + s.to_str().map(str::to_string).map_err_to_string() +} + +fn rstring_to_cstring(s: String) -> CString { + CString::new(s.as_str()).unwrap_or_default() +} + +pub fn update_last_error(err: String) { + println!("Setting LAST_ERROR: {}", err); + LAST_ERROR.with(|prev| { + *prev.borrow_mut() = Some(Box::new(err)); + }); +} + +pub fn take_last_error() -> Option> { + LAST_ERROR.with(|prev| prev.borrow_mut().take()) +} + +#[no_mangle] +pub extern "C" fn last_error_length() -> c_int { + LAST_ERROR.with(|prev| match *prev.borrow() { + Some(ref err) => err.to_string().len() as c_int + 1, + None => 0, + }) +} + +#[no_mangle] +pub unsafe extern "C" fn last_error_message() -> *const c_char { + let last_error = unwrap_safe!( + take_last_error().ok_or("No error found"), + return std::ptr::null_mut() + ); + let error_message = last_error.to_string(); + // println!("Error in last_error_message {error_message}"); + let err = rstring_to_cstring(error_message); + err.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn free_string(s: *mut c_char) { + if s.is_null() { + return; + } + unsafe { + let _ = CString::from_raw(s); + } +} + +#[no_mangle] +pub extern "C" fn new_client( + tenant: *const c_char, + update_frequency: c_ulong, + hostname: *const c_char, +) -> c_int { + let duration = Duration::new(update_frequency, 0); + let tenant = unwrap_safe!(cstring_to_rstring(tenant), return 1); + let hostname = unwrap_safe!(cstring_to_rstring(hostname), return 1); + + // println!("Creating cac client thread for tenant {tenant}"); + let local = task::LocalSet::new(); + local.block_on(&Runtime::new().unwrap(), async move { + match CLIENT_FACTORY + .create_client(tenant.clone(), duration, hostname) + .await + { + Ok(_) => return 0, + Err(err) => { + update_last_error(err); + return 1; + } + } + }); + return 0; +} + +#[no_mangle] +pub extern "C" fn start_polling_update(tenant: *const c_char) { + null_check!(tenant, "NULL pointer provided for tenant", return ()); + unsafe { + let client = get_client(tenant); + null_check!(client, "CAC client for tenant not found", return ()); + let local = task::LocalSet::new(); + // println!("in FFI polling"); + local.block_on( + &Runtime::new().unwrap(), + (*client).clone().run_polling_updates(), + ); + } +} + +#[no_mangle] +pub extern "C" fn free_client(ptr: *mut Arc) { + if ptr.is_null() { + return; + } + unsafe { + let _ = Box::from_raw(ptr); + } +} + +#[no_mangle] +pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc { + let ten = unwrap_safe!(cstring_to_rstring(tenant), return std::ptr::null_mut()); + // println!("fetching cac client thread for tenant {ten}"); + unwrap_safe!( + CLIENT_FACTORY + .get_client(ten) + .map(|client| Box::into_raw(Box::new(client))), + std::ptr::null_mut() + ) +} + +#[no_mangle] +pub extern "C" fn get_last_modified(client: *mut Arc) -> *const c_char { + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + unwrap_safe!( + unsafe { + (*client) + .get_last_modified() + .map(|date| rstring_to_cstring(date.to_string()).into_raw()) + }, + std::ptr::null() + ) +} + +#[no_mangle] +pub extern "C" fn get_config( + client: *mut Arc, + query: *const c_char, +) -> *const c_char { + let filter = if query.is_null() { + None + } else { + let filter_string = match cstring_to_rstring(query) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + let filters: Map = + match serde_json::from_str::>(filter_string.as_str()) { + Ok(json) => json, + Err(err) => { + update_last_error(err.to_string()); + return std::ptr::null(); + } + }; + Some(filters) + }; + + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + unwrap_safe!( + unsafe { + (*client) + .get_full_config_state_with_filter(filter) + .map(|config| { + rstring_to_cstring(serde_json::to_value(config).unwrap().to_string()) + .into_raw() + }) + }, + return std::ptr::null_mut() + ) +} + +#[no_mangle] +pub extern "C" fn get_resolved_config( + client: *mut Arc, + query: *const c_char, + filter_keys: *const c_char, + merge_strategy: *const c_char, +) -> *const c_char { + null_check!( + client, + "an invalid null pointer client is being used, please call get_client()", + return std::ptr::null() + ); + + let keys: Option> = if filter_keys.is_null() { + None + } else { + let filter_string = match cstring_to_rstring(filter_keys) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + Some(filter_string.split("|").map(str::to_string).collect()) + }; + + let query = unwrap_safe!(cstring_to_rstring(query), return std::ptr::null()); + let merge_strategem = + unwrap_safe!(cstring_to_rstring(merge_strategy), return std::ptr::null()); + println!( + "key vector {:#?}, merge strategy {:#?}", + keys, merge_strategem + ); + + let context = unwrap_safe!( + serde_json::from_str::>(query.as_str()), + return std::ptr::null() + ); + + unwrap_safe!( + unsafe { + (*client) + .get_resolved_config(context, keys, MergeStrategy::from(merge_strategem)) + .map(|ov| { + unwrap_safe!( + serde_json::to_string::>(&ov) + .map(|overrides| rstring_to_cstring(overrides).into_raw()), + return std::ptr::null() + ) + }) + }, + return std::ptr::null() + ) +} + +#[no_mangle] +pub extern "C" fn get_default_config( + client: *mut Arc, + filter_keys: *const c_char, +) -> *const c_char { + let keys: Option> = if filter_keys.is_null() { + None + } else { + let filter_string = match cstring_to_rstring(filter_keys) { + Ok(s) => s, + Err(err) => { + update_last_error(err); + return std::ptr::null(); + } + }; + Some(filter_string.split("|").map(str::to_string).collect()) + }; + + unwrap_safe!( + unsafe { + (*client).get_default_config(keys).map(|ov| { + unwrap_safe!( + serde_json::to_string::>(&ov) + .map(|overrides| rstring_to_cstring(overrides).into_raw()), + return std::ptr::null() + ) + }) + }, + return std::ptr::null() + ) +} diff --git a/crates/cac_client/src/lib.rs b/crates/cac_client/src/lib.rs new file mode 100644 index 000000000..84de26842 --- /dev/null +++ b/crates/cac_client/src/lib.rs @@ -0,0 +1,388 @@ +mod eval; +mod interface; +mod utils; + +use actix_web::{rt::time::interval, web::Data}; +use chrono::{DateTime, Utc}; +use derive_more::{Deref, DerefMut}; +use reqwest::{RequestBuilder, Response, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::{ + collections::{HashMap, HashSet}, + convert::identity, + sync::{Arc, RwLock}, + time::{Duration, UNIX_EPOCH}, +}; +use strum_macros; +use utils::core::MapError; + +use service_utils::{ + helpers::extract_dimensions, result as superposition, unexpected_error, +}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Context { + pub condition: Value, + pub override_with_keys: [String; 1], +} + +#[repr(C)] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Config { + pub contexts: Vec, + pub overrides: Map, + pub default_configs: Map, +} + +#[derive(strum_macros::EnumString)] +#[strum(serialize_all = "snake_case")] +pub enum MergeStrategy { + MERGE, + REPLACE, +} + +impl Default for MergeStrategy { + fn default() -> Self { + return Self::MERGE; + } +} + +impl From for MergeStrategy { + fn from(value: String) -> Self { + match value.to_lowercase().as_str() { + "replace" => MergeStrategy::REPLACE, + "merge" => MergeStrategy::MERGE, + _ => MergeStrategy::default(), + } + } +} + +#[repr(C)] +#[derive(Clone)] +pub struct Client { + tenant: String, + reqw: Data, + polling_interval: Duration, + last_modified: Data>>, + config: Data>, +} + +fn clone_reqw(reqw: &RequestBuilder) -> Result { + reqw.try_clone() + .ok_or_else(|| "Unable to clone reqw".to_string()) +} + +fn get_last_modified(resp: &Response) -> Option> { + resp.headers().get("last-modified").and_then(|header_val| { + let header_str = header_val.to_str().ok()?; + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc)) + .map_err(|e| { + log::error!("Failed to parse date: {e}"); + }) + .ok() + }) +} + +impl Client { + pub async fn new( + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result { + let reqw_client = reqwest::Client::builder().build().map_err_to_string()?; + let cac_endpoint = format!("{hostname}/config"); + let reqw = reqw_client + .get(cac_endpoint) + .header("x-tenant", tenant.to_string()); + + let reqwc = clone_reqw(&reqw)?; + let resp = reqwc.send().await.map_err_to_string()?; + let last_modified_at = get_last_modified(&resp); + let config = resp.json::().await.map_err_to_string()?; + + let client = Client { + tenant, + reqw: Data::new(reqw), + polling_interval, + last_modified: Data::new(RwLock::new( + last_modified_at.unwrap_or(DateTime::::from(UNIX_EPOCH)), + )), + config: Data::new(RwLock::new(config)), + }; + Ok(client) + } + + async fn fetch(&self) -> Result { + let last_modified = self.last_modified.read().map_err_to_string()?.to_rfc2822(); + let reqw = clone_reqw(&self.reqw)?.header("If-Modified-Since", last_modified); + let resp = reqw.send().await.map_err_to_string()?; + match resp.status() { + StatusCode::NOT_MODIFIED => { + return Err(String::from(format!( + "{} CAC: skipping update, remote not modified", + self.tenant + ))); + } + StatusCode::OK => log::info!( + "{}", + format!("{} CAC: new config received, updating", self.tenant) + ), + x => return Err(format!("{} CAC: fetch failed, status: {}", self.tenant, x)), + }; + Ok(resp) + } + + async fn update_cac(&self) -> Result { + let fetched_config = self.fetch().await?; + let mut config = self.config.write().map_err_to_string()?; + let mut last_modified = self.last_modified.write().map_err_to_string()?; + let last_modified_at = get_last_modified(&fetched_config); + *config = fetched_config.json::().await.map_err_to_string()?; + if let Some(val) = last_modified_at { + *last_modified = val; + } + Ok(format!("{}: CAC updated successfully", self.tenant)) + } + + pub async fn run_polling_updates(self: Arc) { + let mut interval = interval(self.polling_interval); + loop { + interval.tick().await; + let result = self.update_cac().await.unwrap_or_else(identity); + log::info!("{result}",); + } + } + + pub fn get_full_config_state_with_filter( + &self, + query_data: Option>, + ) -> Result { + let mut config = self.config.read().map(|c| c.clone()).map_err_to_string()?; + if let Some(mut query_map) = query_data { + if let Some(prefix) = query_map.get("prefix") { + let prefix_list: HashSet<&str> = prefix + .as_str() + .ok_or_else(|| { + log::error!("Prefix is not a valid string."); + format!("Prefix is not a valid string.") + }) + .map_err_to_string()? + .split(",") + .collect(); + config = + filter_config_by_prefix(&config, &prefix_list).map_err_to_string()?; + } + + query_map.remove("prefix"); + + if !query_map.is_empty() { + config = filter_config_by_dimensions(&config, &query_map) + .map_err_to_string()?; + } + } + Ok(config) + } + + pub fn get_last_modified(&self) -> Result, String> { + self.last_modified.read().map(|t| *t).map_err_to_string() + } + + pub fn eval( + &self, + query_data: Map, + merge_strategy: MergeStrategy, + ) -> Result, String> { + let cac = self.config.read().map_err_to_string()?; + eval::eval_cac( + cac.default_configs.to_owned(), + &cac.contexts, + &cac.overrides, + &query_data, + merge_strategy, + ) + } + + pub fn get_resolved_config( + &self, + query_data: Map, + filter_keys: Option>, + merge_strategy: MergeStrategy, + ) -> Result, String> { + let mut cac = self.eval(query_data, merge_strategy)?; + if let Some(keys) = filter_keys { + cac = filter_keys_by_prefix(cac, &keys.iter().map(|s| s.as_str()).collect()) + .map_err_to_string()?; + } + return Ok(cac); + } + + pub fn get_default_config( + &self, + filter_keys: Option>, + ) -> Result, String> { + let configs = self.config.read().map_err(|e| e.to_string())?; + let mut default_configs = configs.default_configs.clone(); + if let Some(keys) = filter_keys { + default_configs = filter_keys_by_prefix( + default_configs, + &keys.iter().map(|s| s.as_str()).collect(), + ) + .map_err_to_string()?; + } + return Ok(default_configs); + } +} + +#[derive(Deref, DerefMut)] +pub struct ClientFactory(RwLock>>); +impl ClientFactory { + pub async fn create_client( + &self, + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result, String> { + let mut factory = match self.write() { + Ok(factory) => factory, + Err(e) => { + log::error!("CAC_CLIENT_FACTORY: failed to acquire write lock {}", e); + return Err("CAC_CLIENT_FACTORY: Failed to create client".to_string()); + } + }; + + if let Some(client) = factory.get(&tenant) { + return Ok(client.clone()); + } + + let client = + Arc::new(Client::new(tenant.to_string(), polling_interval, hostname).await?); + factory.insert(tenant.to_string(), client.clone()); + return Ok(client.clone()); + } + + pub fn get_client(&self, tenant: String) -> Result, String> { + let factory = match self.read() { + Ok(factory) => factory, + Err(e) => { + log::error!("CAC_CLIENT_FACTORY: failed to acquire read lock {}", e); + return Err("CAC_CLIENT_FACTORY: Failed to acquire client.".to_string()); + } + }; + + match factory.get(&tenant) { + Some(client) => Ok(client.clone()), + None => Err("No such tenant found".to_string()), + } + } +} + +use once_cell::sync::Lazy; +pub static CLIENT_FACTORY: Lazy = + Lazy::new(|| ClientFactory(RwLock::new(HashMap::new()))); + +pub use eval::eval_cac; +pub use eval::eval_cac_with_reasoning; +pub use eval::merge; + +pub fn filter_keys_by_prefix( + keys: Map, + prefix_list: &HashSet<&str>, +) -> superposition::Result> { + Ok(keys + .into_iter() + .filter(|(key, _)| { + prefix_list + .iter() + .any(|prefix_str| key.starts_with(prefix_str)) + }) + .collect()) +} + +pub fn filter_config_by_prefix( + config: &Config, + prefix_list: &HashSet<&str>, +) -> superposition::Result { + let mut filtered_overrides: Map = Map::new(); + + let filtered_default_config: Map = + filter_keys_by_prefix(config.default_configs.clone(), prefix_list)?; + + for (key, overrides) in &config.overrides { + let overrides_map = overrides + .as_object() + .ok_or_else(|| { + log::error!("failed to decode overrides."); + unexpected_error!("failed to decode overrides.") + })? + .clone(); + + let filtered_overrides_map: Map = overrides_map + .into_iter() + .filter(|(key, _)| filtered_default_config.contains_key(key)) + .collect(); + + if !filtered_overrides_map.is_empty() { + filtered_overrides.insert(key.clone(), Value::Object(filtered_overrides_map)); + } + } + + let filtered_context: Vec = config + .contexts + .clone() + .into_iter() + .filter(|context| filtered_overrides.contains_key(&context.override_with_keys[0])) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: filtered_default_config, + }; + + Ok(filtered_config) +} + +pub fn filter_config_by_dimensions( + config: &Config, + query_params_map: &Map, +) -> superposition::Result { + let filter_context = |contexts: &Vec, + query_params_map: &Map| + -> superposition::Result> { + let mut filtered_context: Vec = Vec::new(); + for context in contexts.iter() { + let dimension = extract_dimensions(&context.condition)?; + let should_add_ctx = dimension.iter().all(|(key, value)| { + query_params_map.get(key).map_or(true, |val| { + val == value || val.as_array().unwrap_or(&vec![]).contains(value) + }) + }); + if should_add_ctx { + filtered_context.push(context.clone()); + } + } + return Ok(filtered_context); + }; + + let filtered_context = filter_context(&config.contexts, &query_params_map)?; + let filtered_overrides: Map = filtered_context + .iter() + .flat_map(|ele| { + let override_with_key = &ele.override_with_keys[0]; + config + .overrides + .get(override_with_key) + .map(|value| (override_with_key.to_string(), value.clone())) + }) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs.clone(), + }; + + Ok(filtered_config) +} diff --git a/crates/cac_client/src/utils/core.rs b/crates/cac_client/src/utils/core.rs new file mode 100644 index 000000000..e00789daf --- /dev/null +++ b/crates/cac_client/src/utils/core.rs @@ -0,0 +1,14 @@ +use std::fmt; + +pub trait MapError { + fn map_err_to_string(self) -> Result; +} + +impl MapError for Result +where + E: fmt::Display, +{ + fn map_err_to_string(self) -> Result { + self.map_err(|e| e.to_string()) + } +} diff --git a/crates/cac_client/src/utils/mod.rs b/crates/cac_client/src/utils/mod.rs new file mode 100644 index 000000000..5a7ca06a4 --- /dev/null +++ b/crates/cac_client/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod core; diff --git a/crates/caclang/CHANGELOG.md b/crates/caclang/CHANGELOG.md new file mode 100644 index 000000000..ec3704f90 --- /dev/null +++ b/crates/caclang/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## caclang-v0.1.0 - 2024-03-06 +#### Features +- support more operations - (4db2c31) - Kartik Gajendra +- added CAC language support - (c549384) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/caclang/Cargo.toml b/crates/caclang/Cargo.toml new file mode 100644 index 000000000..8b4c2ef28 --- /dev/null +++ b/crates/caclang/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "caclang" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "caclang" +path = "src/lib.rs" + +[[bin]] +name = "cac" +path = "src/bin.rs" + + +[dependencies] +toml = "0.8.8" +clap = { version = "4.3.0", features = ["derive"] } +inquire = "0.6.2" +regex = "1.9.1" +serde = "1.0.163" +blake3 = { workspace = true } +anyhow = { workspace = true } +derive_more = { workspace = true } +evalexpr = "11.1.0" +log = { workspace = true } +env_logger = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } diff --git a/crates/caclang/cac_example.toml b/crates/caclang/cac_example.toml new file mode 100644 index 000000000..d6830106a --- /dev/null +++ b/crates/caclang/cac_example.toml @@ -0,0 +1,31 @@ +[dimensions] + +# priority grows exponentially in powers of 2, highest priority items win +height = { type = "number" } # priority 1 +fur-pattern = { type = "string", enum = [ + "fluffy", + "straight", + "nofur", +] } # priority 2 +color = { type = "string", pattern = "a-zA-Z+" } # priority 4 +breed = { type = "string", pattern = "a-zA-Z+" } # priority 8 + +[default-configs] + +name = "dog" +owner = "someone" + +[overrides] + +override_one = { name = "rocky", owner = "Ritick" } +override_two = { name = "bobo", owner = "Shubranshu" } +override_three = { name = "blondie" } +override_four = { owner = "Natarajan" } +override_five = { owner = "Saurav" } + +[contexts] + +"breed = golden_retriever" = ["override_three", "override_five"] +"breed = labrador and height > 3" = ["override_one"] +"breed = pug and fur_pattern = straight" = ["override_two"] +"color = black" = ["override_three", "override_four"] diff --git a/crates/caclang/mjos.toml b/crates/caclang/mjos.toml new file mode 100644 index 000000000..7dcec7ed2 --- /dev/null +++ b/crates/caclang/mjos.toml @@ -0,0 +1,64 @@ +[dimensions] + +# priority grows exponentially in powers of 2 starting with the first entry, highest priority items win +os = { pattern = "(android|ios|web)", type = "string", priority = 1 } # priority 1 +toss = { type = "number", priority = 2 } # priority 2 +clientId = { pattern = "^[a-z0-9].*$", type = "string", priority = 4 } # priority 4 +scope = { pattern = "(beta|release|cug)", type = "string", priority = 8 } # priority 8 +internalUser = { type = "boolean", priority = 16 } # priority 16 +tier = { type = "number", priority = 32 } # priority 32 + +[default-configs] + +android_hyperpay_version = "2.1.1_android" +android_godel_config = "1.0" +android_hyperpay_configuration = "hyperpay configuration android url" +android_hyperpay_strings = "hyperpay strings android url" +android_hyperpay_icons = "hyperpay icons android url" +android_hyperupi_version = "2.2.2_upi_android" +android_hyperupi_configuration = "hyperupi configuration android url" +android_hyperupi_strings = "hyperupi strings android url" +android_hyperupi_icons = "hyperupi icons android url" + +ios_hyperpay_version = "2.1.1_ios" +ios_hyperpay_configuration = "hyperpay configuration ios url" +ios_hyperpay_strings = "hyperpay strings ios url" +ios_hyperpay_icons = "hyperpay icons ios url" +ios_hyperupi_version = "2.2.2_upi_ios" +ios_hyperupi_configuration = "hyperupi configuration ios url" +ios_hyperupi_strings = "hyperupi strings ios url" +ios_hyperupi_icons = "hyperupi icons ios url" + +web_hyperpay_version = "2.1.1_web" +web_hyperpay_configuration = "hyperpay configuration web url" +web_hyperpay_strings = "hyperpay strings web url" +web_hyperpay_icons = "hyperpay icons web url" +web_hyperupi_version = "2.2.2_upi_web" +web_hyperupi_configuration = "hyperupi configuration web url" +web_hyperupi_strings = "hyperupi strings web url" +web_hyperupi_icons = "hyperupi icons web url" + + +["os is android"] +android_godel_config = "1.3" +android_hyperpay_configuration = "you should not this" + +["os is android and clientId in [meesho,zee5]"] +android_hyperupi_version = "1_android" +android_hyperupi_configuration = "PICAF-22147" +android_hyperpay_version = "1.2.1_android" +android_hyperpay_configuration = "PICAF-22145" + +["os is ios and clientId in [a23games,galactustest]"] +ios_hyperupi_version = "10.2.2_upi_ios" + +["os in [web,ios] and toss is -1"] +ios_hyperupi_icons = "PICAF-22146" + +["clientId is cac"] +android_hyperpay_version = "1.2.1_android" +android_hyperpay_configuration = "PICAF-22145" + +["tier in [1,2]"] +android_hyperpay_version = "1.0.1_android" +# godel_config = "1.0" diff --git a/crates/caclang/src/bin.rs b/crates/caclang/src/bin.rs new file mode 100644 index 000000000..8b3e3e944 --- /dev/null +++ b/crates/caclang/src/bin.rs @@ -0,0 +1,58 @@ +use std::fs; + +use caclang::{ContextAwareConfig, DataType}; +use clap::Parser; +use evalexpr::{ContextWithMutableVariables, HashMapContext}; +use inquire::{CustomType, Text}; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct Args { + #[arg(short, long)] + file: String, +} + +pub fn main() -> anyhow::Result<()> { + env_logger::init(); + let args = Args::parse(); + let config = fs::read_to_string(args.file)?; + let cac = ContextAwareConfig::parse(config)?; + loop { + let mut context = HashMapContext::new(); + for (dimension, props) in cac.dimensions.iter() { + if props.data_type == DataType::String { + if let Some(user_value) = Text::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + } else if props.data_type == DataType::Number { + if let Some(user_value) = CustomType::::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + } else { + if let Some(user_value) = CustomType::::new( + format!("Value for {dimension}, hit Esc key to skip").as_str(), + ) + .prompt_skippable()? + { + context.set_value(dimension.clone(), user_value.into())?; + } + }; + } + let v = cac.get_config("android_hyperpay_configuration", &context)?; + println!("config value: {v}"); + if CustomType::::new("Hit Esc to stop, enter to continue") + .prompt_skippable()? + .is_none() + { + return Ok(()); + } + } +} diff --git a/crates/caclang/src/helpers.rs b/crates/caclang/src/helpers.rs new file mode 100644 index 000000000..8684f21bc --- /dev/null +++ b/crates/caclang/src/helpers.rs @@ -0,0 +1,158 @@ +use anyhow::anyhow; +use evalexpr::{build_operator_tree, Node}; +use toml::Table; + +use crate::{DataType, Dimensions}; + +const AND_TOKEN: &str = " and "; +const JOIN_TOKEN: &str = " && "; + +pub(crate) fn extract_section( + item: &mut Table, + section_name: &'static str, +) -> anyhow::Result { + let section = item + .remove(section_name) + .ok_or(anyhow!("{section_name} section not found"))?; + let section = section.as_table().ok_or(anyhow!( + "The formatting of the {section_name} is incorrect. Please check the docs" + ))?; + Ok(section.clone()) +} + +pub fn compute_cac_hash(ctx: &String) -> anyhow::Result { + let tokens = ctx.split(AND_TOKEN).collect::>(); + let mut final_tokens: Vec = Vec::new(); + for token in tokens.into_iter() { + let mut segments = token.trim().split(" "); + let (dimension, op, value) = ( + segments + .next() + .ok_or(anyhow!("Dimension not found for {ctx} in rule {token}"))?, + segments + .next() + .ok_or(anyhow!("Operator not found for {ctx} in rule {token}"))?, + segments + .next() + .ok_or(anyhow!("Value not found for {ctx} in rule {token}"))?, + ); + if op == "IN" { + let mut list_items = + value[1..value.len() - 1].split(",").collect::>(); + list_items.sort(); + let final_list = list_items.join(","); + final_tokens.push(format!("{dimension} {op} [{final_list}]")); + } else { + final_tokens.push(String::from(token)); + } + } + final_tokens.sort(); + let to_be_hashed = final_tokens.join(",").to_string(); + log::debug!("{}", to_be_hashed); + let hash = blake3::hash(to_be_hashed.as_bytes()).to_string(); + log::debug!("hash: {}", hash); + Ok(hash) +} + +pub(crate) fn parse_and_validate_ctx( + dimensions: &Dimensions, + ctx: &String, +) -> anyhow::Result<(Node, u64)> { + let mut expr_ctx: Vec = Vec::new(); + let mut priority: u64 = 0; + for rule in ctx.split(AND_TOKEN).into_iter() { + let parts: Vec<&str> = rule.trim().split_whitespace().collect(); + if parts.len() != 3 { + return Err(anyhow!("Invalid rule {rule} in context {ctx}")); + } + let mut tokens = parts.into_iter(); + let (dimension, operator, value) = ( + tokens + .next() + .ok_or(anyhow!("Dimension not found in {rule}"))?, + tokens + .next() + .ok_or(anyhow!("Operator not found in {rule}"))?, + tokens.next().ok_or(anyhow!("Value not found in {rule}"))?, + ); + let props = dimensions.get(dimension).ok_or(anyhow!( + "Dimension {dimension} is not defined in dimensions section" + ))?; + + let validator = |value| -> anyhow::Result<()> { + if !props.pattern.is_match(value) { + return Err(anyhow!("Invalid value for dimension {dimension} in context {ctx}, the pattern defined does not match with the value provided")); + } + Ok(()) + }; + + let gen_expr = + |op: &str, dimension: &str, value: &str, data_type: &DataType| -> String { + match (op, data_type) { + ("IN", DataType::String) => { + format!("contains(({value}), {dimension})") + } + ("IN", _) => format!("contains(({value}),{dimension})"), + (_, DataType::String) => format!("{dimension} {op} \"{value}\""), + (_, _) => format!("{dimension} {op} {value}"), + } + }; + + let expr_rule = match operator.to_lowercase().as_str() { + "is" => { + validator(value)?; + gen_expr("==", dimension, value, &props.data_type) + } + "=" => { + validator(value)?; + gen_expr("==", dimension, value, &props.data_type) + } + ">=" => { + validator(value)?; + gen_expr(">=", dimension, value, &props.data_type) + } + ">" => { + validator(value)?; + gen_expr(">", dimension, value, &props.data_type) + } + "<=" => { + validator(value)?; + gen_expr("<=", dimension, value, &props.data_type) + } + "<" => { + validator(value)?; + gen_expr("<", dimension, value, &props.data_type) + } + "not" => { + validator(value)?; + gen_expr("!=", dimension, value, &props.data_type) + } + "!=" => { + validator(value)?; + gen_expr("!=", dimension, value, &props.data_type) + } + "in" => { + let values = &value[1..value.len() - 1]; + let mut items: Vec = Vec::new(); + for item in values.split(',').into_iter() { + validator(item)?; + if props.data_type == DataType::String { + items.push(format!("\"{item}\"")); + } else { + items.push(format!("{item}")); + } + } + gen_expr("IN", dimension, items.join(",").as_str(), &props.data_type) + } + _ => { + return Err(anyhow!( + "Unsupported operation {operator} in rule {rule} in context {ctx}" + )) + } + }; + priority += props.priority; + expr_ctx.push(expr_rule); + } + let expr = build_operator_tree(expr_ctx.join(JOIN_TOKEN).as_str())?; + Ok((expr, priority)) +} diff --git a/crates/caclang/src/lib.rs b/crates/caclang/src/lib.rs new file mode 100644 index 000000000..094afd555 --- /dev/null +++ b/crates/caclang/src/lib.rs @@ -0,0 +1,189 @@ +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; +mod helpers; + +use anyhow::anyhow; +use derive_more::Deref; +use evalexpr::Node; +use helpers::{compute_cac_hash, extract_section, parse_and_validate_ctx}; +use regex::Regex; +use strum_macros::EnumString; +use toml::Table; + +pub type HashMapContext = evalexpr::HashMapContext; + +#[derive(Debug, Clone, EnumString, PartialEq)] +pub enum DataType { + #[strum(ascii_case_insensitive)] + String, + #[strum(ascii_case_insensitive)] + Number, + #[strum(ascii_case_insensitive)] + Boolean, +} +#[derive(Debug, Clone)] +pub struct Context { + pub context: String, + pub expr: Node, + pub overrides: HashMap, + pub calculated_priority: u64, +} + +impl Context { + pub fn from( + dimensions: &Dimensions, + default_config: &Table, + ctx: String, + o: &Table, + ) -> anyhow::Result { + let (expr, calculated_priority) = parse_and_validate_ctx(dimensions, &ctx)?; + let mut overrides = HashMap::new(); + for (config_key, ov) in o.into_iter() { + default_config.get(config_key).ok_or(anyhow!("The override key {config_key} for the context {ctx} is not present in default-configs section"))?; + let ove = ov.as_str().ok_or(anyhow!("Invalid syntax for override for the context {ctx} and config key {config_key}"))?.to_string(); + overrides.insert(config_key.into(), ove); + } + Ok(Self { + context: ctx, + expr, + overrides, + calculated_priority, + }) + } +} + +#[derive(Debug, Clone, Deref)] +pub struct Contexts(pub HashMap); + +impl Contexts { + pub fn from( + dimensions: &Dimensions, + default_config: &Table, + value: Table, + ) -> anyhow::Result { + let mut items = HashMap::new(); + for (ctx, overrides) in value.into_iter() { + let ctx_hash = compute_cac_hash(&ctx)?; + if items.contains_key(&ctx_hash) { + let collided_ctx: &Context = items.get(&ctx_hash).ok_or(anyhow!(""))?; + return Err(anyhow!( + "{ctx} is a logical duplicate of {}", + (*collided_ctx).context + )); + } + let overrides = overrides + .as_table() + .ok_or(anyhow!("invalid overrides provided for {ctx}"))?; + items.insert( + ctx_hash, + Context::from(dimensions, default_config, ctx, overrides)?, + ); + } + Ok(Contexts(items)) + } +} + +#[derive(Debug, Clone)] +pub struct DimensionProperties { + pub data_type: DataType, + pub pattern: Regex, + pub priority: u64, +} + +impl DimensionProperties { + fn from(value: &Table) -> anyhow::Result { + let data_type = DataType::from_str( + value + .get("type") + .ok_or(anyhow!("type is missing from dimension"))? + .as_str() + .ok_or(anyhow!( + "Invalid type, allowed values are string, number and boolean" + ))?, + ) + .unwrap(); + let pattern = if let Some(p) = value.get("pattern") { + Regex::new(p.as_str().ok_or(anyhow!("Invalid regex expression"))?)? + } else { + Regex::new(".*")? + }; + let priority = value + .get("priority") + .ok_or(anyhow!("Priority field is missing"))? + .as_integer() + .ok_or(anyhow!("invalid value for priority"))? as u64; + Ok(Self { + data_type, + pattern, + priority, + }) + } +} + +#[derive(Debug, Clone, Deref)] +pub struct Dimensions(pub HashMap); + +impl Dimensions { + pub fn from(value: Table) -> anyhow::Result { + let mut dimension_map = HashMap::new(); + for (dimension, props) in value.into_iter() { + let properties: &Table = props.as_table().unwrap(); + dimension_map.insert(dimension, DimensionProperties::from(properties)?); + } + Ok(Dimensions(dimension_map)) + } +} + +#[derive(Debug, Clone)] +pub struct ContextAwareConfig { + pub dimensions: Dimensions, + pub default_config: Table, + pub contexts: Contexts, +} +impl ContextAwareConfig { + pub fn parse(config: String) -> anyhow::Result { + let mut cac: Table = config.parse::
()?; + let dimensions = Dimensions::from(extract_section(&mut cac, "dimensions")?)?; + let default_config = extract_section(&mut cac, "default-configs")?; + let contexts = Contexts::from(&dimensions, &default_config, cac)?; + Ok(Self { + dimensions, + default_config, + contexts, + }) + } + + pub fn get_config( + &self, + key: &'static str, + context: &HashMapContext, + ) -> anyhow::Result { + let mut sorted_map: BTreeMap> = BTreeMap::new(); + for (_, ctx) in self.contexts.iter() { + let eval = ctx.expr.eval_with_context(context); + if eval.is_ok() && eval?.as_boolean()? { + log::debug!("============================================"); + log::debug!("context {}", ctx.context); + log::debug!("Expression {:?}", ctx.expr.to_string()); + log::debug!("overrides {:?}", ctx.overrides); + log::debug!("priority {:?}", ctx.calculated_priority); + log::debug!("============================================"); + sorted_map.insert(ctx.calculated_priority, &ctx.overrides); + } + } + let mut fin_override: HashMap = HashMap::new(); + for (_, ov) in sorted_map.into_iter() { + fin_override.extend(ov.to_owned()); + } + + Ok(fin_override + .get(key) + .ok_or(anyhow!( + "{key} not found in resolved contexts {:?}", + fin_override + ))? + .to_owned()) + } +} diff --git a/crates/context_aware_config/CHANGELOG.md b/crates/context_aware_config/CHANGELOG.md new file mode 100644 index 000000000..798075d69 --- /dev/null +++ b/crates/context_aware_config/CHANGELOG.md @@ -0,0 +1,333 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## context-aware-config-v0.26.1 - 2024-04-17 +#### Bug Fixes +- [PICAF-26653] removed audit log middleware and reduced max db connection pool size to 2 - (82022eb) - Saurav Suman + +- - - + +## context-aware-config-v0.26.0 - 2024-04-16 +#### Features +- PICAF-26366 Add filter support to client - (f4c12c7) - ankit.mahato + +- - - + +## context-aware-config-v0.25.2 - 2024-04-10 +#### Bug Fixes +- PICAF-26366 added service-prefix to functions endpoints - (5492072) - ankit.mahato +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.25.1 - 2024-04-08 +#### Bug Fixes +- [PICAF-26346] add path to node_modules - (c4bc7b6) - Pratik Mishra + +- - - + +## context-aware-config-v0.25.0 - 2024-04-05 +#### Features +- PICAF-26168-js-secure-sandbox - (566f8be) - Pratik Mishra + +- - - + +## context-aware-config-v0.24.2 - 2024-03-27 +#### Bug Fixes +- PICAF-26454 JS validator functions to take config value and key - (656fe39) - ankit.mahato + +- - - + +## context-aware-config-v0.24.1 - 2024-03-21 +#### Bug Fixes +- PICAF-26307 filter config fix - (aa114fb) - ankit.mahato + +- - - + +## context-aware-config-v0.24.0 - 2024-03-21 +#### Features +- PICAF-26307 Filter Config by prefix - (c0a0bfe) - ankit.mahato + +- - - + +## context-aware-config-v0.23.2 - 2024-03-20 +#### Bug Fixes +- PICAF-25884 Functions bug fixes - (8e7452b) - ankit.mahato + +- - - + +## context-aware-config-v0.23.1 - 2024-03-19 +#### Bug Fixes +- [PICAF-26348] added routes without service prefix for b/w compatibility - (079c02d) - Shubhranshu Sanjeev +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan + +- - - + +## context-aware-config-v0.23.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + +## context-aware-config-v0.22.0 - 2024-03-06 +#### Features +- support more operations - (4db2c31) - Kartik Gajendra + +- - - + +## context-aware-config-v0.21.0 - 2024-03-04 +#### Features +- PICAF-26185 Replace merge-strategy option for resolve/eval - (453cfb9) - ayush.jain@juspay.in + +- - - + +## context-aware-config-v0.20.0 - 2024-03-04 +#### Features +- [PICAF-25877 add node to app directory - (9671875) - Pratik Mishra + +- - - + +## context-aware-config-v0.19.0 - 2024-02-29 +#### Features +- [PICAF-25879] added test,publish api for functions - (050ab24) - Pratik Mishra + +- - - + +## context-aware-config-v0.18.2 - 2024-02-28 +#### Bug Fixes +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik + +- - - + +## context-aware-config-v0.18.1 - 2024-02-26 +#### Bug Fixes +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik + +- - - + +## context-aware-config-v0.18.0 - 2024-02-22 +#### Bug Fixes +- PICAF-26157 Do not remove keys with null value on merge - (bd3c196) - ayush.jain@juspay.in +#### Features +- PICAF-25876 CRUD APIs for function validator - (7c0c963) - ankit.mahato + +- - - + +## context-aware-config-v0.17.0 - 2024-02-22 +#### Features +- [PICAF-25877] js eval with node exec - (adc9b19) - Pratik Mishra + +- - - + +## context-aware-config-v0.16.0 - 2024-02-20 +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.15.2 - 2024-02-19 +#### Bug Fixes +- [PICAF-26004] better logging - (b3d1bc8) - Kartik + +- - - + +## context-aware-config-v0.15.1 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + +## context-aware-config-v0.15.0 - 2024-01-31 +#### Features +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman + +- - - + +## context-aware-config-v0.14.3 - 2024-01-29 +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.14.2 - 2024-01-18 +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.14.1 - 2024-01-12 +#### Bug Fixes +- frontend build process - (cbdad01) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.14.0 - 2024-01-04 +#### Bug Fixes +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +#### Refactoring +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + +## context-aware-config-v0.13.2 - 2023-12-27 +#### Bug Fixes +- [PICAF-25568] array validation for in condition - (a45ac4a) - Pratik Mishra +- PICAF-24961 fix json schema validation - (ed6f814) - ankit.mahato + +- - - + +## context-aware-config-v0.13.1 - 2023-11-22 +#### Bug Fixes +- PICAF-25066 sort json while context creation - (3bd7a97) - Pratik Mishra + +- - - + +## context-aware-config-v0.13.0 - 2023-11-16 +#### Features +- update default keys - (d6b9992) - ankit.mahato + +- - - + +## context-aware-config-v0.12.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## context-aware-config-v0.11.0 - 2023-11-08 +#### Bug Fixes +- make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + +## context-aware-config-v0.10.2 - 2023-10-31 +#### Bug Fixes +- PICAF-25020 x-tenant header mandate removed for OPTIONS calls - (9ee39b5) - Ritick Madaan + +- - - + +## context-aware-config-v0.10.1 - 2023-10-27 +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.10.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.9.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + +## context-aware-config-v0.8.0 - 2023-10-10 +#### Features +- support to update experiment override_keys and variants - (9432bf7) - Shubhranshu Sanjeev +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev + +- - - + +## context-aware-config-v0.7.1 - 2023-10-10 +#### Bug Fixes +- PICAF-24742 add migration for changing default_configs_keys - (55f8895) - Pratik Mishra + +- - - + +## context-aware-config-v0.7.0 - 2023-10-09 +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan +#### Miscellaneous Chores +- database migration for dimensions table - (3a36c56) - Ritick Madaan + +- - - + +## context-aware-config-v0.6.1 - 2023-10-05 +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra + +- - - + +## context-aware-config-v0.6.0 - 2023-10-05 +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra +- PICAF-24664 cors middleware attached - (8cb4805) - Ritick Madaan + +- - - + +## context-aware-config-v0.5.1 - 2023-09-20 +#### Bug Fixes +- PICAF-24507 patching overrides on default-config instead of merge - (2c09e32) - Ritick Madaan + +- - - + +## context-aware-config-v0.5.0 - 2023-09-12 +#### Bug Fixes +- PICAF-24223 eval param fix - (9d4d678) - Pratik Mishra +#### Features +- PICAF-24223 Adding generic eval - (b94ce46) - Pratik Mishra +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + +## context-aware-config-v0.4.0 - 2023-09-06 +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + +## context-aware-config-v0.3.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra + +- - - + +## context-aware-config-v0.2.0 - 2023-09-05 +#### Features +- PICAF-23598 implemented tracing-actix-web for logging - (63dee8c) - Ritick Madaan + +- - - + +## context-aware-config-v0.1.0 - 2023-09-01 +#### Bug Fixes +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- PICAF-24023 sorting same priority contexts with created_at - (24852f5) - Ritick Madaan +- using audit log tstamp for checking last-modified - (2ccaa7e) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- PICAF-23545 updated response-type of /context/bulk-operations api - (1640986) - Ritick Madaan +#### Continuous Integration +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +#### Features +- added log table for all cac_v1 tables - (88a3328) - Shubhranshu Sanjeev +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/context_aware_config/Cargo.toml b/crates/context_aware_config/Cargo.toml new file mode 100644 index 000000000..20534215b --- /dev/null +++ b/crates/context_aware_config/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "context_aware_config" +version = "0.26.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +cac_client = { path = "../cac_client" } +frontend = { path = "../frontend" } + +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +strum_macros = { workspace = true } +strum = { workspace = true } +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +blake3 = { workspace = true} +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +rand = { workspace = true } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +tracing-log = "0.1.3" +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" +futures = "0.3.28" +actix-http = "3.3.1" +futures-util = "0.3.28" +external = { path = "../external" } +actix-cors = "0.6.4" +leptos_actix = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +actix-files = { version = "0.6" } +anyhow = { workspace = true } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } diff --git a/crates/context_aware_config/diesel.toml b/crates/context_aware_config/diesel.toml new file mode 100644 index 000000000..80d2be39f --- /dev/null +++ b/crates/context_aware_config/diesel.toml @@ -0,0 +1,5 @@ +[print_schema] +file = "src/db/schema.rs" + +[migrations_directory] +dir = "migrations" \ No newline at end of file diff --git a/backend/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from backend/migrations/00000000000000_diesel_initial_setup/down.sql rename to crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/up.sql b/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 000000000..d68895b1a --- /dev/null +++ b/crates/context_aware_config/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- 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/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/down.sql b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/down.sql new file mode 100644 index 000000000..291a97c5c --- /dev/null +++ b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/up.sql b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/up.sql new file mode 100644 index 000000000..08ce730c7 --- /dev/null +++ b/crates/context_aware_config/migrations/2023-10-16-133815_context-aware-config-init/up.sql @@ -0,0 +1,191 @@ +-- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- +-- Name: public; Type: SCHEMA; Schema: -; Owner: - +-- +CREATE SCHEMA IF NOT EXISTS public; +-- +-- Name: dimension_type; Type: TYPE; Schema: public; Owner: - +-- +CREATE TYPE public.dimension_type AS ENUM ( + 'NULL', + 'BOOL', + 'NUMBER', + 'STRING', + 'ARRAY', + 'OBJECT' +); +-- +-- 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: contexts; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.contexts ( + id character varying PRIMARY KEY, + value json NOT NULL, + override_id character varying NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + priority integer DEFAULT 1 NOT NULL, + override json DEFAULT '{}'::json NOT NULL +); +-- +-- Name: default_configs; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.default_configs ( + key character varying PRIMARY KEY, + value json NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + schema json DEFAULT '{}'::json NOT NULL +); +-- +-- Name: dimensions; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.dimensions ( + dimension character varying PRIMARY KEY, + priority integer NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by character varying NOT NULL, + schema json DEFAULT '{}'::json NOT NULL +); +-- +-- 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 parititons +-- +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: contexts contexts_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER contexts_audit AFTER INSERT OR DELETE OR UPDATE ON public.contexts FOR EACH ROW EXECUTE FUNCTION public.event_logger(); +-- +-- Name: default_configs default_configs_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER default_configs_audit AFTER INSERT OR DELETE OR UPDATE ON public.default_configs FOR EACH ROW EXECUTE FUNCTION public.event_logger(); +-- +-- Name: dimensions dimensions_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER dimensions_audit AFTER INSERT OR DELETE OR UPDATE ON public.dimensions FOR EACH ROW EXECUTE FUNCTION public.event_logger(); \ No newline at end of file diff --git a/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/down.sql b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/up.sql b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/up.sql new file mode 100644 index 000000000..dbd5c77b2 --- /dev/null +++ b/crates/context_aware_config/migrations/2024-01-23-123559_audit_log_partitions/up.sql @@ -0,0 +1,118 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS public.event_log_y2024m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-08-01') TO ('2024-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-09-01') TO ('2024-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-10-01') TO ('2024-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-11-01') TO ('2024-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-12-01') TO ('2025-01-01'); + +------------ Parititions for 2025 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2025m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-01-01') TO ('2025-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-02-01') TO ('2025-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-03-01') TO ('2025-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-04-01') TO ('2025-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-05-01') TO ('2025-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-06-01') TO ('2025-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-07-01') TO ('2025-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-08-01') TO ('2025-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-09-01') TO ('2025-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-10-01') TO ('2025-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-11-01') TO ('2025-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-12-01') TO ('2026-01-01'); + +------------ Parititions for 2026 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2026m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-01-01') TO ('2026-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-02-01') TO ('2026-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-03-01') TO ('2026-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-04-01') TO ('2026-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-05-01') TO ('2026-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-06-01') TO ('2026-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-07-01') TO ('2026-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-08-01') TO ('2026-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-09-01') TO ('2026-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-10-01') TO ('2026-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-11-01') TO ('2026-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-12-01') TO ('2027-01-01'); \ No newline at end of file diff --git a/crates/context_aware_config/migrations/2024-02-19-125126_functions/down.sql b/crates/context_aware_config/migrations/2024-02-19-125126_functions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context_aware_config/migrations/2024-02-19-125126_functions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context_aware_config/migrations/2024-02-19-125126_functions/up.sql b/crates/context_aware_config/migrations/2024-02-19-125126_functions/up.sql new file mode 100644 index 000000000..73c8c0cbc --- /dev/null +++ b/crates/context_aware_config/migrations/2024-02-19-125126_functions/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here +-- Name: functions; Type: TABLE; Schema: public; Owner: - +-- +CREATE TABLE public.functions ( + function_name text PRIMARY KEY, + published_code text, + draft_code text NOT NULL, + function_description text NOT NULL, + published_runtime_version VARCHAR(16), + draft_runtime_version VARCHAR(16) NOT NULL, + published_at timestamp without time zone, + draft_edited_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + published_by text, + draft_edited_by text NOT NULL +); +-- +-- Name: functions functions_audit; Type: TRIGGER; Schema: public; Owner: - +-- +CREATE TRIGGER functions_audit AFTER INSERT OR DELETE OR UPDATE ON public.functions FOR EACH ROW EXECUTE FUNCTION public.event_logger(); \ No newline at end of file diff --git a/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql new file mode 100644 index 000000000..6e74d4c11 --- /dev/null +++ b/crates/context_aware_config/migrations/2024-03-05-122806_dimensions_functions_ref/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +ALTER TABLE public.dimensions ADD COLUMN function_name text NULL; + +ALTER TABLE public.dimensions ADD FOREIGN KEY(function_name) REFERENCES public.functions(function_name); + +ALTER TABLE public.default_configs ADD COLUMN function_name text NULL; + +ALTER TABLE public.default_configs ADD FOREIGN KEY(function_name) REFERENCES public.functions(function_name); \ No newline at end of file diff --git a/crates/context_aware_config/package-lock.json b/crates/context_aware_config/package-lock.json new file mode 100644 index 000000000..61fc6b17d --- /dev/null +++ b/crates/context_aware_config/package-lock.json @@ -0,0 +1,1070 @@ +{ + "name": "context-aware-config", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "context-aware-config", + "version": "0.0.1", + "dependencies": { + "axios": "^0.16.1", + "eslint": "^8.56.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/axios": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.16.2.tgz", + "integrity": "sha512-IMYFDrcVbUksQhsMYtWCM6KdNaDpr1NY56dpzaIgj92ecPVI29bf2sOgAf8aGTiq8UoixJD61Pj0Ahej5DPv7w==", + "deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410", + "dependencies": { + "follow-redirects": "^1.2.3", + "is-buffer": "^1.1.5" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/crates/context_aware_config/package.json b/crates/context_aware_config/package.json new file mode 100644 index 000000000..621cf3646 --- /dev/null +++ b/crates/context_aware_config/package.json @@ -0,0 +1,11 @@ +{ + "name": "context-aware-config", + "version": "0.0.1", + "private": true, + "description": "dependencies for CAC function validators", + "dependencies": { + "axios": "^0.16.1", + "eslint": "^8.56.0" + + } +} diff --git a/samples/default_config.json b/crates/context_aware_config/samples/default_config.json similarity index 100% rename from samples/default_config.json rename to crates/context_aware_config/samples/default_config.json diff --git a/samples/dimensions.json b/crates/context_aware_config/samples/dimensions.json similarity index 100% rename from samples/dimensions.json rename to crates/context_aware_config/samples/dimensions.json diff --git a/samples/overrides.json b/crates/context_aware_config/samples/overrides.json similarity index 100% rename from samples/overrides.json rename to crates/context_aware_config/samples/overrides.json diff --git a/crates/context_aware_config/src/api/audit_log/handlers.rs b/crates/context_aware_config/src/api/audit_log/handlers.rs new file mode 100644 index 000000000..57f88e32c --- /dev/null +++ b/crates/context_aware_config/src/api/audit_log/handlers.rs @@ -0,0 +1,63 @@ +use actix_web::{get, web::Query, HttpResponse, Scope}; +use chrono::{Duration, Utc}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::json; +use service_utils::{result as superposition, service::types::DbConnection}; + +use crate::{api::audit_log::types::AuditQueryFilters, db::models::EventLog}; + +use crate::db::schema::event_log::dsl as event_log; + +pub fn endpoints() -> Scope { + Scope::new("").service(get_audit_logs) +} + +#[get("")] +async fn get_audit_logs( + filters: Query, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + + let query_builder = |filters: &AuditQueryFilters| { + let mut builder = event_log::event_log.into_boxed(); + if let Some(tables) = filters.table.clone() { + builder = builder.filter(event_log::table_name.eq_any(tables.0)); + } + if let Some(actions) = filters.action.clone() { + builder = builder.filter(event_log::action.eq_any(actions.0)); + } + if let Some(username) = filters.username.clone() { + builder = builder.filter(event_log::user_name.eq(username)); + } + let now = Utc::now().naive_utc(); + builder + .filter( + event_log::timestamp + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(event_log::timestamp.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(event_log::timestamp.desc()) + .limit(limit) + .offset(offset); + + let log_count: i64 = count_query.count().get_result(&mut conn)?; + + let logs: Vec = query.load(&mut conn)?; + + 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 + }))) +} diff --git a/crates/context_aware_config/src/api/audit_log/mod.rs b/crates/context_aware_config/src/api/audit_log/mod.rs new file mode 100644 index 000000000..9e1802e17 --- /dev/null +++ b/crates/context_aware_config/src/api/audit_log/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +mod types; + +pub use handlers::endpoints; diff --git a/crates/context_aware_config/src/api/audit_log/types.rs b/crates/context_aware_config/src/api/audit_log/types.rs new file mode 100644 index 000000000..dd7aa554b --- /dev/null +++ b/crates/context_aware_config/src/api/audit_log/types.rs @@ -0,0 +1,19 @@ +use chrono::NaiveDateTime; +use serde::Deserialize; +use service_utils::helpers::deserialize_stringified_list; + +#[derive(Deserialize, Debug, Clone)] +pub struct StringArgs( + #[serde(deserialize_with = "deserialize_stringified_list")] pub Vec, +); + +#[derive(Debug, Clone, Deserialize)] +pub struct AuditQueryFilters { + pub from_date: Option, + pub to_date: Option, + pub table: Option, + pub action: Option, + pub username: Option, + pub count: Option, + pub page: Option, +} diff --git a/crates/context_aware_config/src/api/config/handlers.rs b/crates/context_aware_config/src/api/config/handlers.rs new file mode 100644 index 000000000..73b6bc2e4 --- /dev/null +++ b/crates/context_aware_config/src/api/config/handlers.rs @@ -0,0 +1,343 @@ +use std::collections::HashSet; +use std::{collections::HashMap, str::FromStr}; + +use super::helpers::{ + filter_config_by_dimensions, filter_config_by_prefix, filter_context, +}; + +use super::types::Config; +use crate::db::schema::{ + contexts::dsl as ctxt, default_configs::dsl as def_conf, event_log::dsl as event_log, +}; +use actix_http::header::{HeaderName, HeaderValue}; +use actix_web::{get, web::Query, HttpRequest, HttpResponse, Scope}; +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 serde_json::{json, Map, Value}; +use service_utils::service::types::DbConnection; +use service_utils::{bad_argument, db_error, unexpected_error}; + +use service_utils::result as superposition; +use uuid::Uuid; + +pub fn endpoints() -> Scope { + Scope::new("") + .service(get) + .service(get_resolved_config) + .service(get_filtered_config) +} + +pub fn add_audit_header( + conn: &mut PooledConnection>, + mut res: HttpResponse, +) -> superposition::Result { + let header_name = HeaderName::from_static("x-audit-id"); + 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()) + .first::(conn) + { + let uuid_string = uuid.to_string(); + if let Ok(header_value) = HeaderValue::from_str(&uuid_string) { + res.headers_mut().insert(header_name, header_value); + } else { + log::error!("Failed to convert UUID to string"); + } + } else { + log::error!("Failed to fetch contexts from event_log"); + } + Ok(res) +} + +fn add_last_modified_header( + max_created_at: Option, + mut res: HttpResponse, +) -> superposition::Result { + let header_name = HeaderName::from_static("last-modified"); + + if let Some(ele) = max_created_at { + let datetime_utc: DateTime = TimeZone::from_utc_datetime(&Utc, &ele); + let value = HeaderValue::from_str(&DateTime::to_rfc2822(&datetime_utc)); + if let Ok(header_value) = value { + res.headers_mut().insert(header_name, header_value); + } + } + Ok(res) +} + +fn get_max_created_at( + conn: &mut PooledConnection>, +) -> Result { + event_log::event_log + .select(max(event_log::timestamp)) + .filter(event_log::table_name.eq_any(vec!["contexts", "default_configs"])) + .first::>(conn) + .and_then(|res| res.ok_or(diesel::result::Error::NotFound)) +} + +fn is_not_modified(max_created_at: Option, req: &HttpRequest) -> bool { + let nanosecond_erasure = |t: NaiveDateTime| t.with_nanosecond(0); + let last_modified = req + .headers() + .get("If-Modified-Since") + .and_then(|header_val| { + let header_str = header_val.to_str().ok()?; + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) + .ok() + }) + .and_then(nanosecond_erasure); + log::info!("last modified {last_modified:?}"); + let parsed_max: Option = max_created_at.and_then(nanosecond_erasure); + max_created_at.is_some() && parsed_max <= last_modified +} + +async fn generate_cac( + conn: &mut PooledConnection>, +) -> superposition::Result { + let contexts_vec = ctxt::contexts + .select((ctxt::id, ctxt::value, ctxt::override_id, ctxt::override_)) + .order_by((ctxt::priority.asc(), ctxt::created_at.asc())) + .load::<(String, Value, String, Value)>(conn) + .map_err(|err| { + log::error!("failed to fetch contexts with error: {}", err); + db_error!(err) + })?; + + let (contexts, overrides) = contexts_vec.into_iter().fold( + (Vec::new(), Map::new()), + |(mut ctxts, mut overrides), (id, condition, override_id, override_)| { + let ctxt = super::types::Context { + id, + condition, + override_with_keys: [override_id.to_owned()], + }; + ctxts.push(ctxt); + overrides.insert(override_id, override_); + (ctxts, overrides) + }, + ); + + let default_config_vec = def_conf::default_configs + .select((def_conf::key, def_conf::value)) + .load::<(String, Value)>(conn) + .map_err(|err| { + log::error!("failed to fetch default_configs with error: {}", err); + db_error!(err) + })?; + + let default_configs = + default_config_vec + .into_iter() + .fold(Map::new(), |mut acc, item| { + acc.insert(item.0, item.1); + acc + }); + + Ok(Config { + contexts, + overrides, + default_configs, + }) +} + +#[get("")] +async fn get( + req: HttpRequest, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + + let max_created_at = get_max_created_at(&mut conn) + .map_err(|e| log::error!("failed to fetch max timestamp from event_log: {e}")) + .ok(); + + log::info!("Max created at: {max_created_at:?}"); + + let is_not_modified = is_not_modified(max_created_at, &req); + + if is_not_modified { + return Ok(HttpResponse::NotModified().finish()); + } + + let params = Query::>::from_query(req.query_string()) + .map_err(|err| { + log::error!("Failed to parse query params with err: {}", err); + bad_argument!("Unable to retrieve query parameters.") + })?; + let mut query_params_map: serde_json::Map = Map::new(); + + for (key, value) in params.0.into_iter() { + query_params_map.insert( + key, + value + .parse::() + .map_or_else(|_| json!(value), |int_val| json!(int_val)), + ); + } + + let mut config = generate_cac(&mut conn).await?; + if let Some(prefix) = query_params_map.get("prefix") { + let prefix_list: HashSet<&str> = prefix + .as_str() + .ok_or_else(|| { + log::error!("Prefix is not a valid string."); + bad_argument!("Prefix is not a valid string") + })? + .split(",") + .collect(); + config = filter_config_by_prefix(&config, &prefix_list)? + } + + query_params_map.remove("prefix"); + + if !query_params_map.is_empty() { + config = filter_config_by_dimensions(&config, &query_params_map)? + } + + let resp = HttpResponse::Ok().json(config); + let audit_resp = add_audit_header(&mut conn, resp)?; + + add_last_modified_header(max_created_at, audit_resp) +} + +#[get("/resolve")] +async fn get_resolved_config( + req: HttpRequest, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let params = Query::>::from_query(req.query_string()) + .map_err(|err| { + log::error!("failed to parse query params with err: {}", err); + bad_argument!("error getting query params") + })?; + + let mut query_params_map: serde_json::Map = Map::new(); + + for item in params.0.into_iter() { + query_params_map.insert( + item.0, + item.1 + .parse::() + .map_or_else(|_| json!(item.1), |int_val| json!(int_val)), + ); + } + + let max_created_at = get_max_created_at(&mut conn) + .map_err(|e| log::error!("failed to fetch max timestamp from event_log : {e}")) + .ok(); + + let is_not_modified = is_not_modified(max_created_at, &req); + + if is_not_modified { + return Ok(HttpResponse::NotModified().finish()); + } + + let res = generate_cac(&mut conn).await?; + + let cac_client_contexts = res + .contexts + .into_iter() + .map(|val| cac_client::Context { + condition: val.condition, + override_with_keys: val.override_with_keys, + }) + .collect(); + + let merge_strategy = req + .headers() + .get("x-merge-strategy") + .and_then(|header_value: &HeaderValue| header_value.to_str().ok()) + .and_then(|val| MergeStrategy::from_str(val).ok()) + .unwrap_or(MergeStrategy::default()); + + let response = if let Some(Value::String(_)) = query_params_map.get("show_reasoning") + { + HttpResponse::Ok().json( + eval_cac_with_reasoning( + res.default_configs, + &cac_client_contexts, + &res.overrides, + &query_params_map, + merge_strategy, + ) + .map_err(|err| { + log::error!("failed to eval cac with err: {}", err); + unexpected_error!("cac eval failed") + })?, + ) + } else { + HttpResponse::Ok().json( + eval_cac( + res.default_configs, + &cac_client_contexts, + &res.overrides, + &query_params_map, + merge_strategy, + ) + .map_err(|err| { + log::error!("failed to eval cac with err: {}", err); + unexpected_error!("cac eval failed") + })?, + ) + }; + let audit_resp = add_audit_header(&mut conn, response)?; + add_last_modified_header(max_created_at, audit_resp) +} + +#[get("/filter")] +async fn get_filtered_config( + req: HttpRequest, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let params = Query::>::from_query(req.query_string()) + .map_err(|err| { + log::error!("failed to parse query params with err: {}", err); + bad_argument!("Error getting query params.") + })?; + let mut query_params_map: serde_json::Map = Map::new(); + + for (key, value) in params.0.into_iter() { + query_params_map.insert( + key, + value + .parse::() + .map_or_else(|_| json!(value), |int_val| json!(int_val)), + ); + } + let config = generate_cac(&mut conn).await?; + let contexts = config.contexts; + + let filtered_context = filter_context(&contexts, &query_params_map)?; + let mut filtered_overrides: Map = Map::new(); + for ele in filtered_context.iter() { + let override_with_key = &ele.override_with_keys[0]; + filtered_overrides.insert( + override_with_key.to_string(), + config + .overrides + .get(override_with_key) + .ok_or_else(|| { + log::error!("Could not fetch override_with_key"); + unexpected_error!("Something went wrong") + })? + .to_owned(), + ); + } + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs, + }; + + add_audit_header(&mut conn, HttpResponse::Ok().json(filtered_config)) +} diff --git a/crates/context_aware_config/src/api/config/helpers.rs b/crates/context_aware_config/src/api/config/helpers.rs new file mode 100644 index 000000000..4b2fd0d65 --- /dev/null +++ b/crates/context_aware_config/src/api/config/helpers.rs @@ -0,0 +1,110 @@ +use std::collections::HashSet; + +use super::types::{Config, Context}; + +use serde_json::{Map, Value}; +use service_utils::{ + helpers::extract_dimensions, result as superposition, unexpected_error, +}; + +pub fn filter_context( + contexts: &Vec, + query_params_map: &Map, +) -> superposition::Result> { + let mut filtered_context: Vec = Vec::new(); + for context in contexts.iter() { + if should_add_ctx(&context, query_params_map)? { + filtered_context.push(context.clone()); + } + } + return Ok(filtered_context); +} + +fn should_add_ctx( + context: &Context, + query_params_map: &Map, +) -> superposition::Result { + let dimension = extract_dimensions(&context.condition)?; + Ok(dimension.iter().all(|(key, value)| { + query_params_map.get(key).map_or(true, |val| { + val == value || val.as_array().unwrap_or(&vec![]).contains(value) + }) + })) +} + +pub fn filter_config_by_prefix( + config: &Config, + prefix_list: &HashSet<&str>, +) -> superposition::Result { + let mut filtered_overrides: Map = Map::new(); + + let filtered_default_config: Map = config + .default_configs + .clone() + .into_iter() + .filter(|(key, _)| { + prefix_list + .iter() + .any(|prefix_str| key.starts_with(prefix_str)) + }) + .collect(); + + for (key, overrides) in &config.overrides { + let overrides_map = overrides + .as_object() + .ok_or_else(|| { + log::error!("failed to decode overrides."); + unexpected_error!("failed to decode overrides.") + })? + .clone(); + + let filtered_overrides_map: Map = overrides_map + .into_iter() + .filter(|(key, _)| filtered_default_config.contains_key(key)) + .collect(); + + if !filtered_overrides_map.is_empty() { + filtered_overrides.insert(key.clone(), Value::Object(filtered_overrides_map)); + } + } + + let filtered_context: Vec = config + .contexts + .clone() + .into_iter() + .filter(|context| filtered_overrides.contains_key(&context.override_with_keys[0])) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: filtered_default_config, + }; + + Ok(filtered_config) +} + +pub fn filter_config_by_dimensions( + config: &Config, + query_params_map: &Map, +) -> superposition::Result { + let filtered_context = filter_context(&config.contexts, &query_params_map)?; + let filtered_overrides: Map = filtered_context + .iter() + .flat_map(|ele| { + let override_with_key = &ele.override_with_keys[0]; + config + .overrides + .get(override_with_key) + .map(|value| (override_with_key.to_string(), value.clone())) + }) + .collect(); + + let filtered_config = Config { + contexts: filtered_context, + overrides: filtered_overrides, + default_configs: config.default_configs.clone(), + }; + + Ok(filtered_config) +} diff --git a/crates/context_aware_config/src/api/config/mod.rs b/crates/context_aware_config/src/api/config/mod.rs new file mode 100644 index 000000000..d4417bb8b --- /dev/null +++ b/crates/context_aware_config/src/api/config/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +mod types; +pub use handlers::endpoints; +mod helpers; diff --git a/crates/context_aware_config/src/api/config/types.rs b/crates/context_aware_config/src/api/config/types.rs new file mode 100644 index 000000000..332bfc03f --- /dev/null +++ b/crates/context_aware_config/src/api/config/types.rs @@ -0,0 +1,16 @@ +use serde::Serialize; +use serde_json::{Map, Value}; + +#[derive(Serialize)] +pub struct Config { + pub contexts: Vec, + pub overrides: Map, + pub default_configs: Map, +} + +#[derive(Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} diff --git a/crates/context_aware_config/src/api/context/handlers.rs b/crates/context_aware_config/src/api/context/handlers.rs new file mode 100644 index 000000000..df7f0fe34 --- /dev/null +++ b/crates/context_aware_config/src/api/context/handlers.rs @@ -0,0 +1,525 @@ +extern crate base64; +use std::str; + +use crate::helpers::{json_to_sorted_string, validate_context_jsonschema}; +use crate::{ + api::{ + context::types::{ + ContextAction, ContextBulkResponse, DimensionCondition, MoveReq, + PaginationParams, PutReq, PutResp, + }, + dimension::get_all_dimension_schema_map, + }, + db::{ + models::Context, + schema::{ + contexts::{self, id}, + default_configs::dsl, + }, + }, +}; +use actix_web::{ + delete, get, put, + web::{Json, Path, Query}, + HttpResponse, Responder, Scope, +}; +use chrono::Utc; +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}; +use service_utils::service::types::DbConnection; +use service_utils::{db_error, not_found, unexpected_error, validation_error}; +use std::collections::HashMap; +use superposition_types::{SuperpositionUser, User}; + +use super::helpers::{ + validate_condition_with_functions, validate_override_with_functions, +}; + +use service_utils::{bad_argument, result as superposition}; + +pub fn endpoints() -> Scope { + Scope::new("") + .service(put_handler) + .service(move_handler) + .service(delete_context) + .service(bulk_operations) + .service(list_contexts) + .service(get_context) +} + +type DBConnection = PooledConnection>; + +fn validate_dimensions_and_calculate_priority( + object_key: &str, + cond: &Value, + dimension_schema_map: &HashMap, +) -> superposition::Result { + let get_priority = |key: &String, val: &Value| -> superposition::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(|(_, priority)| priority) + .ok_or(bad_argument!( + "No matching dimension ({}) found", + dimension_name + )) + .copied() + } else { + validate_dimensions_and_calculate_priority(key, val, dimension_schema_map) + } + }; + + match cond { + Value::Object(x) => x.iter().try_fold(0, |acc, (key, val)| { + get_priority(key, val).map(|res| res + acc) + }), + Value::Array(arr) => { + let mut val: Option = None; + let mut condition: Option = None; + for i in arr { + if let (None, Ok(x)) = + (&condition, from_value::(json!(i))) + { + condition = Some(x); + } else if val == 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_value_schema, _) = 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_value_schema, + )?; + } + arr.iter().try_fold(0, |acc, item| { + validate_dimensions_and_calculate_priority( + object_key, + item, + dimension_schema_map, + ) + .map(|res| res + acc) + }) + } + _ => Ok(0), + } +} + +fn validate_override_with_default_configs( + conn: &mut DBConnection, + override_: &Map, +) -> 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)) + .get_results::<(String, Value)>(conn)?; + + let map = Map::from_iter(res); + + for (key, value) in override_.iter() { + let schema = map + .get(key) + // .map(|resp| resp) + .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} with error {:?}", + verrors + )); + }; + } + + Ok(()) +} + +fn create_ctx_from_put_req( + req: Json, + conn: &mut DBConnection, + user: &User, +) -> superposition::Result { + let ctx_condition = Value::Object(req.context.to_owned()); + let ctx_override: Value = req.r#override.to_owned().into(); + validate_override_with_default_configs(conn, &req.r#override)?; + validate_condition_with_functions(conn, &ctx_condition)?; + validate_override_with_functions(conn, &req.r#override)?; + + let dimension_schema_map = get_all_dimension_schema_map(conn)?; + + let priority = validate_dimensions_and_calculate_priority( + "context", + &ctx_condition, + &dimension_schema_map, + )?; + + if priority == 0 { + return Err(bad_argument!("No dimension found in context")); + } + + let context_id = hash(&ctx_condition); + let override_id = hash(&ctx_override); + Ok(Context { + id: context_id.clone(), + value: ctx_condition, + priority, + override_id: override_id.to_owned(), + override_: ctx_override.to_owned(), + created_at: Utc::now(), + created_by: user.get_email(), + }) +} + +fn hash(val: &Value) -> String { + let sorted_str: String = json_to_sorted_string(val); + blake3::hash(sorted_str.as_bytes()).to_string() +} + +fn update_override_of_existing_ctx( + conn: &mut PgConnection, + ctx: Context, +) -> superposition::Result { + use contexts::dsl; + let mut new_override: Value = dsl::contexts + .filter(dsl::id.eq(&ctx.id)) + .select(dsl::override_) + .first(conn)?; + cac_client::merge(&mut new_override, &ctx.override_); + let new_override_id = hash(&new_override); + let new_ctx = Context { + override_: new_override, + override_id: new_override_id, + ..ctx + }; + diesel::update(dsl::contexts) + .filter(dsl::id.eq(&new_ctx.id)) + .set(&new_ctx) + .execute(conn)?; + Ok(get_put_resp(new_ctx)) +} + +fn get_put_resp(ctx: Context) -> PutResp { + PutResp { + context_id: ctx.id, + override_id: ctx.override_id, + priority: ctx.priority, + } +} + +fn put( + req: Json, + conn: &mut PooledConnection>, + already_under_txn: bool, + user: &User, +) -> superposition::Result { + use contexts::dsl::contexts; + let new_ctx = create_ctx_from_put_req(req, conn, user)?; + + if already_under_txn { + diesel::sql_query("SAVEPOINT put_ctx_savepoint").execute(conn)?; + } + let insert = diesel::insert_into(contexts).values(&new_ctx).execute(conn); + + match insert { + Ok(_) => Ok(get_put_resp(new_ctx)), + Err(DatabaseError(UniqueViolation, _)) => { + if already_under_txn { + diesel::sql_query("ROLLBACK TO put_ctx_savepoint").execute(conn)?; + } + update_override_of_existing_ctx(conn, new_ctx) + } + Err(e) => { + log::error!("failed to update context with db error: {:?}", e); + Err(db_error!(e)) + } + } +} + +#[put("")] +async fn put_handler( + req: Json, + mut db_conn: DbConnection, + user: User, +) -> superposition::Result> { + put(req, &mut db_conn, false, &user) + .map(|resp| Json(resp)) + .map_err(|err: superposition::AppError| { + log::info!("context put failed with error: {:?}", err); + err + }) +} + +fn r#move( + old_ctx_id: String, + req: Json, + conn: &mut PooledConnection>, + already_under_txn: bool, + user: &User, +) -> superposition::Result { + use contexts::dsl; + let req = req.into_inner(); + let ctx_condition = Value::Object(req.context); + let new_ctx_id = hash(&ctx_condition); + let dimension_schema_map = get_all_dimension_schema_map(conn)?; + let priority = validate_dimensions_and_calculate_priority( + "context", + &ctx_condition, + &dimension_schema_map, + )?; + + if priority == 0 { + return Err(bad_argument!("no dimension found in context")); + } + + 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), + dsl::priority.eq(priority), + )) + .get_result(conn); + + let contruct_new_ctx_with_old_overrides = |ctx: Context| Context { + id: new_ctx_id, + value: ctx_condition, + priority, + created_at: Utc::now(), + created_by: user.get_email(), + override_id: ctx.override_id, + override_: ctx.override_, + }; + + 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)) + .get_result(db_conn)?; + + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(db_conn, ctx) + } else { + db_conn.build_transaction().read_write().run(|conn| { + let deleted_ctxt = diesel::delete(dsl::contexts) + .filter(dsl::id.eq(&old_ctx_id)) + .get_result(conn)?; + let ctx = contruct_new_ctx_with_old_overrides(deleted_ctxt); + update_override_of_existing_ctx(conn, ctx) + }) + } + }; + + match context { + Ok(ctx) => Ok(get_put_resp(ctx)), + 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)) + } + } +} + +#[put("/move/{ctx_id}")] +async fn move_handler( + path: Path, + req: Json, + mut db_conn: DbConnection, + user: User, +) -> superposition::Result> { + r#move(path.into_inner(), req, &mut db_conn, false, &user) + .map(|resp| Json(resp)) + .map_err(|err| { + log::info!("move api failed with error: {:?}", err); + err + }) +} + +#[get("/{ctx_id}")] +async fn get_context( + path: Path, + db_conn: DbConnection, +) -> superposition::Result { + use crate::db::schema::contexts::dsl::*; + + let ctx_id = path.into_inner(); + let DbConnection(mut conn) = db_conn; + + let ctx: Context = contexts + .filter(id.eq(ctx_id)) + .get_result::(&mut conn)?; + + Ok(Json(ctx)) +} + +#[get("/list")] +async fn list_contexts( + qparams: Query, + db_conn: DbConnection, +) -> superposition::Result { + use crate::db::schema::contexts::dsl::*; + let DbConnection(mut conn) = db_conn; + + let PaginationParams { + page: opt_page, + size: opt_size, + } = qparams.into_inner(); + let default_page = 1; + let page = opt_page.unwrap_or(default_page); + let default_size = 20; + let size = opt_size.unwrap_or(default_size); + + if page < 1 { + return Err(bad_argument!("Param 'page' has to be at least 1.")); + } else if size < 1 { + return Err(bad_argument!("Param 'size' has to be at least 1.")); + } + + let result: Vec = contexts + .order(created_at) + .limit(i64::from(size)) + .offset(i64::from(size * (page - 1))) + .load(&mut conn)?; + + Ok(Json(result)) +} + +#[delete("/{ctx_id}")] +async fn delete_context( + path: Path, + db_conn: DbConnection, + user: User, +) -> superposition::Result { + use contexts::dsl; + let DbConnection(mut conn) = db_conn; + + let ctx_id = path.into_inner(); + let deleted_row = + delete(dsl::contexts.filter(dsl::id.eq(&ctx_id))).execute(&mut 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(HttpResponse::NoContent().finish()) + } + Err(e) => { + log::error!("context delete query failed with error: {e}"); + Err(unexpected_error!("Something went wrong.")) + } + } +} + +#[put("/bulk-operations")] +async fn bulk_operations( + reqs: Json>, + db_conn: DbConnection, + user: User, +) -> superposition::Result>> { + use contexts::dsl::contexts; + let DbConnection(mut conn) = db_conn; + + let mut response = Vec::::new(); + conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { + for action in reqs.into_inner().into_iter() { + match action { + ContextAction::PUT(put_req) => { + let put_resp = put(Json(put_req), transaction_conn, true, &user) + .map_err(|err| { + log::error!( + "Failed at insert into contexts due to {:?}", + err + ); + err + })?; + response.push(ContextBulkResponse::PUT(put_resp)); + } + ContextAction::DELETE(ctx_id) => { + let deleted_row = + delete(contexts.filter(id.eq(&ctx_id))).execute(transaction_conn); + let email: String = user.get_email(); + 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) => { + return Err(bad_argument!( + "context with id {} not found", + ctx_id + )) + } + Ok(_) => { + log::info!("{ctx_id} context deleted by {email}"); + response.push(ContextBulkResponse::DELETE(format!( + "{ctx_id} deleted succesfully" + ))) + } + Err(e) => { + log::error!("Delete context failed due to {:?}", e); + return Err(db_error!(e)); + } + }; + } + ContextAction::MOVE((old_ctx_id, move_req)) => { + let move_context_resp = + r#move(old_ctx_id, Json(move_req), transaction_conn, true, &user) + .map_err(|err| { + log::error!( + "Failed at moving context reponse due to {:?}", + err + ); + err + })?; + response.push(ContextBulkResponse::MOVE(move_context_resp)); + } + } + } + Ok(()) // Commit the transaction + })?; + Ok(Json(response)) +} diff --git a/crates/context_aware_config/src/api/context/helpers.rs b/crates/context_aware_config/src/api/context/helpers.rs new file mode 100644 index 000000000..caae40a77 --- /dev/null +++ b/crates/context_aware_config/src/api/context/helpers.rs @@ -0,0 +1,136 @@ +extern crate base64; +use base64::prelude::*; +use service_utils::helpers::extract_dimensions; +use service_utils::{result as superposition, unexpected_error, validation_error}; +use std::str; + +use crate::api::functions::helpers::get_published_functions_by_names; +use crate::validation_functions::execute_fn; +use crate::{ + api::context::types::FunctionsInfo, + db::schema::{ + default_configs::dsl, + dimensions::{self}, + }, +}; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use serde_json::{Map, Value}; +use std::collections::HashMap; +type DBConnection = PooledConnection>; + +pub fn validate_condition_with_functions( + conn: &mut DBConnection, + context: &Value, +) -> superposition::Result<()> { + use dimensions::dsl; + let context = extract_dimensions(&context)?; + let dimensions_list: Vec = context.keys().cloned().collect(); + let keys_function_array: Vec<(String, Option)> = dsl::dimensions + .filter(dsl::dimension.eq_any(dimensions_list)) + .select((dsl::dimension, dsl::function_name)) + .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)?; + for (key, value) in context.iter() { + if let Some(functions_map) = dimension_functions_map.get(key) { + if let (function_name, Some(function_code)) = + (functions_map.name.clone(), functions_map.code.clone()) + { + validate_value_with_function(&function_name, &function_code, key, value)?; + } + } + } + Ok(()) +} + +pub fn validate_override_with_functions( + conn: &mut DBConnection, + override_: &Map, +) -> superposition::Result<()> { + let default_config_keys: Vec = override_.keys().cloned().collect(); + let keys_function_array: Vec<(String, Option)> = dsl::default_configs + .filter(dsl::key.eq_any(default_config_keys)) + .select((dsl::key, dsl::function_name)) + .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 default_config_functions_map = get_functions_map(conn, new_keys_function_array)?; + for (key, value) in override_.iter() { + if let Some(functions_map) = default_config_functions_map.get(key) { + if let (function_name, Some(function_code)) = + (functions_map.name.clone(), functions_map.code.clone()) + { + validate_value_with_function(&function_name, &function_code, key, value)?; + } + } + } + Ok(()) +} + +fn get_functions_map( + conn: &mut DBConnection, + keys_function_array: Vec<(String, String)>, +) -> superposition::Result> { + let functions_map: HashMap> = + get_published_functions_by_names( + conn, + keys_function_array + .iter() + .map(|(_, f_name)| f_name.clone()) + .collect(), + )? + .into_iter() + .collect(); + + let default_config_functions_map: HashMap = + keys_function_array + .into_iter() + .map(|(key, function_name)| { + ( + key.clone(), + FunctionsInfo { + name: function_name.clone(), + code: functions_map.get(&function_name).cloned().flatten(), + }, + ) + }) + .collect(); + Ok(default_config_functions_map) +} + +pub fn validate_value_with_function( + fun_name: &str, + function: &str, + key: &String, + value: &Value, +) -> superposition::Result<()> { + let base64_decoded = BASE64_STANDARD.decode(function).map_err(|err| { + log::error!("Failed to decode function code: {}", err); + unexpected_error!("Failed to decode function code: {}", err) + })?; + let utf8_decoded = str::from_utf8(&base64_decoded).map_err(|err| { + log::error!("Failed to parse function code in UTF-8: {}", err); + unexpected_error!("Failed to parse function code in UTF-8: {}", err) + })?; + if let Err((err, stdout)) = execute_fn(&utf8_decoded, key, value.to_owned()) { + let stdout = stdout.unwrap_or(String::new()); + log::error!("function validation failed for {key} with error: {err}"); + return Err(validation_error!( + "Function validation failed for {} with error {}. {}", + key, + err, + stdout + )); + } + Ok(()) +} diff --git a/crates/context_aware_config/src/api/context/mod.rs b/crates/context_aware_config/src/api/context/mod.rs new file mode 100644 index 000000000..8c255ad45 --- /dev/null +++ b/crates/context_aware_config/src/api/context/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +pub mod helpers; +mod types; +pub use handlers::endpoints; diff --git a/crates/context_aware_config/src/api/context/types.rs b/crates/context_aware_config/src/api/context/types.rs new file mode 100644 index 000000000..c376fbd15 --- /dev/null +++ b/crates/context_aware_config/src/api/context/types.rs @@ -0,0 +1,51 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +#[derive(Deserialize, Clone)] +pub struct PutReq { + pub context: Map, + pub r#override: Map, +} + +#[derive(Deserialize, Clone)] +pub struct MoveReq { + pub context: Map, +} + +#[derive(Deserialize, Clone)] +pub struct DimensionCondition { + pub var: String, +} + +#[derive(Serialize, Debug)] +pub struct PutResp { + pub context_id: String, + pub override_id: String, + pub priority: i32, +} + +#[derive(Deserialize)] +pub struct PaginationParams { + pub page: Option, + pub size: Option, +} + +#[derive(serde::Deserialize)] +pub enum ContextAction { + PUT(PutReq), + DELETE(String), + MOVE((String, MoveReq)), +} + +#[derive(serde::Serialize)] +pub enum ContextBulkResponse { + PUT(PutResp), + DELETE(String), + MOVE(PutResp), +} + +#[derive(Deserialize, Clone)] +pub struct FunctionsInfo { + pub name: String, + pub code: Option, +} diff --git a/crates/context_aware_config/src/api/default_config/handlers.rs b/crates/context_aware_config/src/api/default_config/handlers.rs new file mode 100644 index 000000000..6f2755cb7 --- /dev/null +++ b/crates/context_aware_config/src/api/default_config/handlers.rs @@ -0,0 +1,184 @@ +extern crate base64; +use super::types::CreateReq; +use service_utils::{bad_argument, unexpected_error, validation_error}; + +use superposition_types::{SuperpositionUser, User}; + +use crate::api::context::helpers::validate_value_with_function; +use crate::{ + api::functions::helpers::get_published_function_code, + db::{self, models::DefaultConfig, schema::default_configs::dsl::default_configs}, + helpers::validate_jsonschema, +}; +use actix_web::{ + get, put, + web::{self, Data, Json}, + HttpResponse, Scope, +}; +use chrono::Utc; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Value}; +use service_utils::{ + result as superposition, + service::types::{AppState, DbConnection}, +}; + +pub fn endpoints() -> Scope { + Scope::new("").service(create).service(get) +} + +#[put("/{key}")] +async fn create( + state: Data, + key: web::Path, + request: web::Json, + db_conn: DbConnection, + user: User, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + let key = key.into_inner(); + + 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.")); + } + + 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) = match result { + Ok((val, schema, f_name)) => { + let val = req.value.unwrap_or_else(|| val); + let schema = req.schema.map_or_else(|| schema, Value::Object); + let f_name = if req.function_name == Some(Value::Null) { + None + } else { + func_name.or(f_name) + }; + (val, schema, f_name) + } + Err(superposition::AppError::DbError(diesel::NotFound)) => { + match (req.value, req.schema) { + (Some(val), Some(schema)) => (val, Value::Object(schema), func_name), + _ => { + 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 default_config = DefaultConfig { + key: key.to_owned(), + value, + schema, + function_name, + created_by: user.get_email(), + created_at: Utc::now(), + }; + + validate_jsonschema( + &state.default_config_validation_schema, + &default_config.schema, + )?; + + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&default_config.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!("Invalid JSON schema (failed to compile)")); + } + }; + + if let Err(e) = jschema.validate(&default_config.value) { + let verrors = e.collect::>(); + log::info!( + "Validation for value with given JSON schema failed: {:?}", + verrors + ); + return Err(validation_error!( + "Schema validation failed for key {} with error {:?}", + default_config.key, + verrors + )); + } + + 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, + )?; + } + } + + let upsert = diesel::insert_into(default_configs) + .values(&default_config) + .on_conflict(db::schema::default_configs::key) + .do_update() + .set(&default_config) + .execute(&mut conn); + + match upsert { + Ok(_) => Ok(HttpResponse::Ok().json(json!({ + "message": "DefaultConfig created/updated successfully." + }))), + Err(e) => { + log::info!("DefaultConfig creation failed with error: {e}"); + Err(unexpected_error!( + "Something went wrong, failed to create DefaultConfig" + )) + } + } +} + +fn fetch_default_key( + key: &String, + conn: &mut PooledConnection>, +) -> superposition::Result<(Value, Value, Option)> { + let res: (Value, Value, Option) = default_configs + .filter(db::schema::default_configs::key.eq(key)) + .select(( + db::schema::default_configs::value, + db::schema::default_configs::schema, + db::schema::default_configs::function_name, + )) + .get_result::<(Value, Value, Option)>(conn)?; + Ok(res) +} + +#[get("")] +async fn get(db_conn: DbConnection) -> superposition::Result>> { + let DbConnection(mut conn) = db_conn; + + let result: Vec = default_configs.get_results(&mut conn)?; + Ok(Json(result)) +} diff --git a/crates/context_aware_config/src/api/default_config/mod.rs b/crates/context_aware_config/src/api/default_config/mod.rs new file mode 100644 index 000000000..ebe17b924 --- /dev/null +++ b/crates/context_aware_config/src/api/default_config/mod.rs @@ -0,0 +1,3 @@ +mod handlers; +mod types; +pub use handlers::endpoints; diff --git a/crates/context_aware_config/src/api/default_config/types.rs b/crates/context_aware_config/src/api/default_config/types.rs new file mode 100644 index 000000000..0e4b1b615 --- /dev/null +++ b/crates/context_aware_config/src/api/default_config/types.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Deserializer}; +use serde_json::{Map, Value}; + +#[derive(Debug, Deserialize)] +pub struct CreateReq { + #[serde(default, deserialize_with = "deserialize_option")] + pub value: Option, + pub schema: Option>, + #[serde(default, deserialize_with = "deserialize_option")] + pub function_name: Option, +} + +fn deserialize_option<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(Some(value)) +} diff --git a/crates/context_aware_config/src/api/dimension/handlers.rs b/crates/context_aware_config/src/api/dimension/handlers.rs new file mode 100644 index 000000000..0092be472 --- /dev/null +++ b/crates/context_aware_config/src/api/dimension/handlers.rs @@ -0,0 +1,112 @@ +use crate::{ + api::dimension::types::CreateReq, + db::{models::Dimension, schema::dimensions::dsl::*}, + helpers::validate_jsonschema, +}; +use actix_web::{ + get, put, + web::{self, Data, Json}, + HttpResponse, Scope, +}; +use chrono::Utc; +use diesel::RunQueryDsl; +use jsonschema::{Draft, JSONSchema}; +use serde_json::Value; +use service_utils::{ + bad_argument, result as superposition, + service::types::{AppState, DbConnection}, + unexpected_error, +}; + +use superposition_types::{SuperpositionUser, User}; + +pub fn endpoints() -> Scope { + Scope::new("").service(create).service(get) +} + +#[put("")] +async fn create( + state: Data, + req: web::Json, + user: User, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + + if req.priority <= 0 { + return Err(bad_argument!("Priority should be greater than 0")); + } + + let create_req = req.into_inner(); + let schema_value = create_req.schema; + + validate_jsonschema(&state.meta_schema, &schema_value)?; + + let schema_compile_result = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&schema_value); + + if let Err(e) = schema_compile_result { + return Err(bad_argument!( + "Invalid JSON schema (failed to compile): {:?}", + e + )); + }; + + let fun_name = match create_req.function_name { + Some(Value::String(func_name)) => Some(func_name), + Some(Value::Null) | None => None, + _ => { + log::error!("Expected a string or null as the function name."); + return Err(bad_argument!( + "Expected a string or null as the function name." + )); + } + }; + + let new_dimension = Dimension { + dimension: create_req.dimension, + priority: i32::from(create_req.priority), + schema: schema_value, + created_by: user.get_email(), + created_at: Utc::now(), + function_name: fun_name.clone(), + }; + + let upsert = diesel::insert_into(dimensions) + .values(&new_dimension) + .on_conflict(dimension) + .do_update() + .set(&new_dimension) + .get_result::(&mut conn); + + match upsert { + Ok(upserted_dimension) => { + return Ok(HttpResponse::Created().json(upserted_dimension)) + } + Err(diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::ForeignKeyViolation, + e, + )) => { + log::error!("{fun_name:?} function not found with error: {e:?}"); + return Err(bad_argument!( + "Funtion {} doesn't exists", + fun_name.unwrap_or(String::new()) + )); + } + Err(e) => { + log::error!("Dimension upsert failed with error: {e}"); + return Err(unexpected_error!( + "Something went wrong, failed to create/update dimension" + )); + } + } +} + +#[get("")] +async fn get(db_conn: DbConnection) -> superposition::Result>> { + let DbConnection(mut conn) = db_conn; + + let result: Vec = dimensions.get_results(&mut conn)?; + Ok(Json(result)) +} diff --git a/crates/context_aware_config/src/api/dimension/mod.rs b/crates/context_aware_config/src/api/dimension/mod.rs new file mode 100644 index 000000000..6088a1053 --- /dev/null +++ b/crates/context_aware_config/src/api/dimension/mod.rs @@ -0,0 +1,5 @@ +mod handlers; +mod types; +mod utils; +pub use handlers::endpoints; +pub use utils::get_all_dimension_schema_map; diff --git a/crates/context_aware_config/src/api/dimension/types.rs b/crates/context_aware_config/src/api/dimension/types.rs new file mode 100644 index 000000000..1c81a69e1 --- /dev/null +++ b/crates/context_aware_config/src/api/dimension/types.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Deserializer}; +use serde_json::Value; + +#[derive(Debug, Deserialize)] +pub struct CreateReq { + pub dimension: String, + pub priority: u16, + pub schema: Value, + #[serde(default, deserialize_with = "deserialize_option")] + pub function_name: Option, +} + +fn deserialize_option<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value: Value = Deserialize::deserialize(deserializer)?; + Ok(Some(value)) +} diff --git a/crates/context_aware_config/src/api/dimension/utils.rs b/crates/context_aware_config/src/api/dimension/utils.rs new file mode 100644 index 000000000..07b6c9342 --- /dev/null +++ b/crates/context_aware_config/src/api/dimension/utils.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use crate::db::{models::Dimension, schema::dimensions::dsl::*}; +use diesel::RunQueryDsl; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + PgConnection, +}; +use jsonschema::{Draft, JSONSchema}; +use service_utils::result as superposition; + +pub fn get_all_dimension_schema_map( + conn: &mut PooledConnection>, +) -> superposition::Result> { + let dimensions_vec = dimensions.load::(conn)?; + + let dimension_schema_map = dimensions_vec + .into_iter() + .filter_map(|item| { + let compiled_schema = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&item.schema) + .ok()?; + + Some((item.dimension, (compiled_schema, i32::from(item.priority)))) + }) + .collect(); + + Ok(dimension_schema_map) +} diff --git a/crates/context_aware_config/src/api/functions/handlers.rs b/crates/context_aware_config/src/api/functions/handlers.rs new file mode 100644 index 000000000..5322f231d --- /dev/null +++ b/crates/context_aware_config/src/api/functions/handlers.rs @@ -0,0 +1,287 @@ +extern crate base64; +use base64::prelude::*; + +use super::helpers::{decode_function, fetch_function}; + +use crate::{ + api::functions::types::{Stage, TestFunctionRequest, TestParam}, + db::{ + self, + models::Function, + schema::functions::{dsl, dsl::functions, function_name}, + }, + validation_functions, +}; +use actix_web::{ + delete, get, patch, post, put, + web::{self, Json, Path}, + HttpResponse, Result, Scope, +}; +use chrono::Utc; +use diesel::{delete, ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::json; +use service_utils::{bad_argument, not_found, service::types::DbConnection}; + +use superposition_types::{SuperpositionUser, User}; + +use service_utils::{result as superposition, unexpected_error}; +use validation_functions::{compile_fn, execute_fn}; + +use super::types::{CreateFunctionRequest, UpdateFunctionRequest}; + +pub fn endpoints() -> Scope { + Scope::new("") + .service(create) + .service(update) + .service(get) + .service(list_functions) + .service(delete_function) + .service(test) + .service(publish) +} + +#[post("")] +async fn create( + request: web::Json, + db_conn: DbConnection, + user: User, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + + compile_fn(&req.function)?; + + let function = Function { + function_name: req.function_name, + draft_code: BASE64_STANDARD.encode(req.function), + draft_runtime_version: req.runtime_version, + draft_edited_by: user.get_email(), + draft_edited_at: Utc::now().naive_utc(), + published_code: None, + published_at: None, + published_by: None, + published_runtime_version: None, + function_description: req.description, + }; + + let insert: Result = diesel::insert_into(functions) + .values(&function) + .get_result(&mut conn); + + match insert { + Ok(mut res) => { + decode_function(&mut res)?; + Ok(Json(res)) + } + Err(e) => match e { + diesel::result::Error::DatabaseError(kind, e) => { + log::error!("Function error: {:?}", e); + match kind { + diesel::result::DatabaseErrorKind::UniqueViolation => { + return Err(bad_argument!("Function already exists.")) + } + _ => { + return Err(unexpected_error!( + "Something went wrong, failed to create function" + )) + } + } + } + _ => { + log::error!("Function creation failed with error: {e}"); + return Err(unexpected_error!( + "An error occured please contact the admin." + )); + } + }, + } +} + +#[patch("/{function_name}")] +async fn update( + params: web::Path, + request: web::Json, + db_conn: DbConnection, + user: User, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let req = request.into_inner(); + let f_name = params.into_inner(); + + let result = match fetch_function(&f_name, &mut conn) { + Ok(val) => val, + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { + log::error!("Function not found."); + return Err(bad_argument!("Function {} doesn't exists", f_name)); + } + Err(e) => { + log::error!("Failed to update Function with error: {e}"); + return Err(unexpected_error!("Failed to update Function")); + } + }; + + // Function Linter Check + if let Some(function) = &req.function { + compile_fn(function)?; + } + + let new_function = Function { + function_name: f_name.to_owned(), + draft_code: req.function.map_or_else( + || result.draft_code.clone(), + |func| BASE64_STANDARD.encode(func), + ), + draft_runtime_version: req + .runtime_version + .unwrap_or(result.draft_runtime_version), + function_description: req.description.unwrap_or(result.function_description), + draft_edited_by: user.get_email(), + draft_edited_at: Utc::now().naive_utc(), + published_code: result.published_code, + published_at: result.published_at, + published_by: result.published_by, + published_runtime_version: result.published_runtime_version, + }; + + let mut updated_function = diesel::update(functions) + .filter(db::schema::functions::function_name.eq(f_name)) + .set(new_function) + .get_result::(&mut conn)?; + + decode_function(&mut updated_function)?; + Ok(Json(updated_function)) +} + +#[get("/{function_name}")] +async fn get( + params: web::Path, + db_conn: DbConnection, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let f_name = params.into_inner(); + let mut function = fetch_function(&f_name, &mut conn)?; + + decode_function(&mut function)?; + Ok(Json(function)) +} + +#[get("")] +async fn list_functions( + db_conn: DbConnection, +) -> superposition::Result>> { + let DbConnection(mut conn) = db_conn; + let mut function_list = functions.get_results(&mut conn)?; + for function in function_list.iter_mut() { + decode_function(function)?; + } + Ok(Json(function_list)) +} + +#[delete("/{function_name}")] +async fn delete_function( + params: web::Path, + db_conn: DbConnection, + user: User, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let f_name = params.into_inner(); + + let deleted_row = + delete(functions.filter(function_name.eq(&f_name))).execute(&mut conn); + match deleted_row { + Ok(0) => Err(not_found!("Function {} doesn't exists", f_name)), + Ok(_) => { + log::info!("{f_name} function deleted by {}", user.get_email()); + Ok(HttpResponse::NoContent().finish()) + } + Err(e) => { + log::error!("function delete query failed with error: {e}"); + Err(unexpected_error!( + "Something went wrong, failed to delete the function" + )) + } + } +} + +#[put("/{function_name}/{stage}/test")] +async fn test( + params: Path, + request: web::Json, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + let path_params = params.into_inner(); + let fun_name = &path_params.function_name; + let req = request.into_inner(); + let mut function = match fetch_function(fun_name, &mut conn) { + Ok(val) => val, + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { + log::error!("Function not found."); + return Err(bad_argument!("Function {} doesn't exists", fun_name)); + } + Err(e) => { + log::error!("Failed to fetch Function {fun_name} with error: {e}"); + return Err(unexpected_error!( + "Something went wrong, failed to update function" + )); + } + }; + + decode_function(&mut function)?; + let result = match path_params.stage { + Stage::DRAFT => execute_fn(&function.draft_code, &req.key, req.value), + Stage::PUBLISHED => match function.published_code { + Some(code) => execute_fn(&code, &req.key, req.value), + None => { + log::error!("Function test failed: function not published yet"); + Err(( + "Function test failed as function not published yet".to_owned(), + None, + )) + } + }, + }; + + match result { + Ok(stdout) => Ok(HttpResponse::Ok() + .json(json!({"message": "Function validated the given value successfully", "stdout": stdout}))), + Err((e, stdout)) => Err(bad_argument!("Function validation failed with error: {}, stdout: {:?}", e, stdout )), + } +} + +#[put("/{function_name}/publish")] +async fn publish( + params: web::Path, + db_conn: DbConnection, + user: User, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let fun_name = params.into_inner(); + + let function = match fetch_function(&fun_name, &mut conn) { + Ok(val) => val, + Err(superposition::AppError::DbError(diesel::result::Error::NotFound)) => { + log::error!("Function {} not found.", fun_name); + return Err(bad_argument!("Function {} doesn't exists", fun_name)); + } + Err(e) => { + log::error!("Failed to update Function with error: {e}"); + return Err(unexpected_error!( + "Something went wrong, failed to update function" + )); + } + }; + + let updated_function = diesel::update(functions) + .filter(dsl::function_name.eq(fun_name.clone())) + .set(( + dsl::published_code.eq(Some(function.draft_code.clone())), + dsl::published_runtime_version + .eq(Some(function.draft_runtime_version.clone())), + dsl::published_by.eq(Some(user.get_email())), + dsl::published_at.eq(Some(Utc::now().naive_utc())), + )) + .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 new file mode 100644 index 000000000..643364f34 --- /dev/null +++ b/crates/context_aware_config/src/api/functions/helpers.rs @@ -0,0 +1,69 @@ +extern crate base64; +use base64::prelude::*; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; +use service_utils::{result as superposition, unexpected_error}; +use std::str; + +use crate::db::{self, models::Function, schema::functions::dsl::functions}; + +pub fn fetch_function( + f_name: &String, + conn: &mut PooledConnection>, +) -> superposition::Result { + Ok(functions + .filter(db::schema::functions::function_name.eq(f_name)) + .get_result::(conn)?) +} + +pub fn decode_function(function: &mut Function) -> superposition::Result<()> { + function.draft_code = decode_base64_to_string(&function.draft_code)?; + if let Some(code) = &function.published_code { + function.published_code = Some(decode_base64_to_string(&code)?); + } + Ok(()) +} + +pub fn decode_base64_to_string(code: &String) -> superposition::Result { + BASE64_STANDARD + .decode(code) + .map_err(|e| { + log::info!("Error while decoding function: {}", e); + unexpected_error!("Failed to decode function") + }) + .and_then(|decoded_code| { + str::from_utf8(&decoded_code) + .map(|res| res.to_string()) + .map_err(|e| { + log::info!("Error while decoding function: {}", e); + unexpected_error!("Something went wrong, failed to decode function") + }) + }) +} + +pub fn get_published_function_code( + conn: &mut PooledConnection>, + f_name: String, +) -> superposition::Result> { + let function = functions + .filter(db::schema::functions::function_name.eq(f_name)) + .select(db::schema::functions::published_code) + .first(conn)?; + Ok(function) +} + +pub fn get_published_functions_by_names( + conn: &mut PooledConnection>, + function_names: Vec, +) -> superposition::Result)>> { + let function: Vec<(String, Option)> = functions + .filter(db::schema::functions::function_name.eq_any(function_names)) + .select(( + db::schema::functions::function_name, + db::schema::functions::published_code, + )) + .load(conn)?; + Ok(function) +} diff --git a/crates/context_aware_config/src/api/functions/mod.rs b/crates/context_aware_config/src/api/functions/mod.rs new file mode 100644 index 000000000..8ebb62780 --- /dev/null +++ b/crates/context_aware_config/src/api/functions/mod.rs @@ -0,0 +1,4 @@ +mod handlers; +pub use handlers::endpoints; +pub mod helpers; +mod types; diff --git a/crates/context_aware_config/src/api/functions/types.rs b/crates/context_aware_config/src/api/functions/types.rs new file mode 100644 index 000000000..39a39b40c --- /dev/null +++ b/crates/context_aware_config/src/api/functions/types.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Deserialize)] +pub struct UpdateFunctionRequest { + pub function: Option, + pub runtime_version: Option, + pub description: Option, +} + +#[derive(Debug, Deserialize)] +pub struct CreateFunctionRequest { + pub function_name: String, + pub function: String, + pub runtime_version: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionResponse { + pub function_name: String, + pub function: String, + pub function_description: String, + pub runtime_version: String, + pub status: String, + pub published_at: String, + pub drafted_at: String, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] +pub enum Stage { + DRAFT, + PUBLISHED, +} + +#[derive(Deserialize)] +pub struct TestParam { + pub function_name: String, + pub stage: Stage, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TestFunctionRequest { + pub key: String, + pub value: Value, +} diff --git a/crates/context_aware_config/src/api/mod.rs b/crates/context_aware_config/src/api/mod.rs new file mode 100644 index 000000000..6616da3b8 --- /dev/null +++ b/crates/context_aware_config/src/api/mod.rs @@ -0,0 +1,6 @@ +pub mod audit_log; +pub mod config; +pub mod context; +pub mod default_config; +pub mod dimension; +pub mod functions; diff --git a/backend/src/db/mod.rs b/crates/context_aware_config/src/db/mod.rs similarity index 50% rename from backend/src/db/mod.rs rename to crates/context_aware_config/src/db/mod.rs index 0b41f16a7..d5cbad7e2 100644 --- a/backend/src/db/mod.rs +++ b/crates/context_aware_config/src/db/mod.rs @@ -1,2 +1,2 @@ -pub mod utils; +pub mod models; pub mod schema; diff --git a/crates/context_aware_config/src/db/models.rs b/crates/context_aware_config/src/db/models.rs new file mode 100644 index 000000000..86e275522 --- /dev/null +++ b/crates/context_aware_config/src/db/models.rs @@ -0,0 +1,76 @@ +use crate::db::schema::{contexts, default_configs, dimensions, event_log, functions}; +use chrono::{offset::Utc, DateTime, NaiveDateTime}; +use diesel::{AsChangeset, Insertable, Queryable, Selectable}; +use serde::Serialize; +use serde_json::Value; + +#[derive(Queryable, Selectable, Insertable, AsChangeset, Clone, Serialize, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(id))] +pub struct Context { + pub id: String, + pub value: Value, + pub override_id: String, + pub created_at: DateTime, + pub created_by: String, + pub priority: i32, + #[serde(rename(serialize = "override"))] + pub override_: Value, +} + +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(dimension))] +#[diesel(treat_none_as_null = true)] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub created_at: DateTime, + pub created_by: String, + pub schema: Value, + pub function_name: Option, +} + +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(key))] +#[diesel(treat_none_as_null = true)] +pub struct DefaultConfig { + pub key: String, + pub value: Value, + pub created_at: DateTime, + pub created_by: String, + pub schema: Value, + pub function_name: Option, +} + +#[derive(Queryable, Selectable, Insertable, AsChangeset, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(name))] +pub struct Function { + pub function_name: String, + pub published_code: Option, + pub draft_code: String, + pub function_description: String, + pub published_runtime_version: Option, + pub draft_runtime_version: String, + pub published_at: Option, + pub draft_edited_at: NaiveDateTime, + pub published_by: Option, + pub draft_edited_by: String, +} + +#[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(table_name = event_log)] +#[diesel(primary_key(id))] +pub struct EventLog { + pub id: uuid::Uuid, + pub table_name: String, + pub user_name: String, + pub timestamp: NaiveDateTime, + pub action: String, + pub original_data: Option, + pub new_data: Option, + pub query: String, +} diff --git a/crates/context_aware_config/src/db/schema.rs b/crates/context_aware_config/src/db/schema.rs new file mode 100644 index 000000000..99d135ac1 --- /dev/null +++ b/crates/context_aware_config/src/db/schema.rs @@ -0,0 +1,651 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + contexts (id) { + id -> Varchar, + value -> Json, + override_id -> Varchar, + created_at -> Timestamptz, + created_by -> Varchar, + priority -> Int4, + #[sql_name = "override"] + override_ -> Json, + } +} + +diesel::table! { + default_configs (key) { + key -> Varchar, + value -> Json, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + function_name -> Nullable, + } +} + +diesel::table! { + dimensions (dimension) { + dimension -> Varchar, + priority -> Int4, + created_at -> Timestamptz, + created_by -> Varchar, + schema -> Json, + function_name -> Nullable, + } +} + +diesel::table! { + event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + functions (function_name) { + function_name -> Text, + published_code -> Nullable, + draft_code -> Text, + function_description -> Text, + #[max_length = 16] + published_runtime_version -> Nullable, + #[max_length = 16] + draft_runtime_version -> Varchar, + published_at -> Nullable, + draft_edited_at -> Timestamp, + published_by -> Nullable, + draft_edited_by -> Text, + } +} + +diesel::joinable!(default_configs -> functions (function_name)); +diesel::joinable!(dimensions -> functions (function_name)); + +diesel::allow_tables_to_appear_in_same_query!( + contexts, + default_configs, + dimensions, + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, + event_log_y2024m08, + event_log_y2024m09, + event_log_y2024m10, + event_log_y2024m11, + event_log_y2024m12, + event_log_y2025m01, + event_log_y2025m02, + event_log_y2025m03, + event_log_y2025m04, + event_log_y2025m05, + event_log_y2025m06, + event_log_y2025m07, + event_log_y2025m08, + event_log_y2025m09, + event_log_y2025m10, + event_log_y2025m11, + event_log_y2025m12, + event_log_y2026m01, + event_log_y2026m02, + event_log_y2026m03, + event_log_y2026m04, + event_log_y2026m05, + event_log_y2026m06, + event_log_y2026m07, + event_log_y2026m08, + event_log_y2026m09, + event_log_y2026m10, + event_log_y2026m11, + event_log_y2026m12, + functions, +); diff --git a/crates/context_aware_config/src/helpers.rs b/crates/context_aware_config/src/helpers.rs new file mode 100644 index 000000000..8b0d943c8 --- /dev/null +++ b/crates/context_aware_config/src/helpers.rs @@ -0,0 +1,383 @@ +use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; +use itertools::{self, Itertools}; +use jsonschema::{Draft, JSONSchema, ValidationError}; +use serde_json::{json, Value}; +use service_utils::{result as superposition, validation_error}; +use std::collections::HashMap; + +pub fn get_default_config_validation_schema() -> JSONSchema { + let my_schema = json!( + { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array" + } + ] + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "string" + } + } + }, + "then": { + "oneOf": [ + { + "required": ["pattern"], + "properties": { "pattern": { "type": "string" } } + }, + { + "required": ["enum"], + "properties": { + "enum": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 1 + }, + } + } + ] + } + } + // TODO: Add validations for Array types. + ] + }); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&my_schema) + .expect("THE IMPOSSIBLE HAPPENED, failed to compile the schema for the schema!") +} + +pub fn parse_headermap_safe(headermap: &HeaderMap) -> HashMap { + let mut req_headers = HashMap::new(); + let record_header = |(header_name, header_val): (&HeaderName, &HeaderValue)| { + let header_val = match header_val.to_str() { + Ok(s) => String::from(s), + Err(e) => { + log::error!( + "unable to parse value of header {}, error: {e}", + header_name + ); + String::from("Error: non ASCII header value") + } + }; + req_headers.insert(header_name.to_string(), header_val); + }; + headermap.iter().for_each(record_header); + req_headers +} + +pub fn get_meta_schema() -> JSONSchema { + let my_schema = json!({ + "type": "object", + "properties": { + "type": { + "enum": ["boolean", "number", "string"] + }, + }, + "required": ["type"], + + // # Add extra validation if needed for other primitive data types + "if": { + "properties": { "type": { "const": "string" } } + } + , "then": { + "oneOf": [ + { + "required": ["pattern"], + "properties": { "pattern": { "type": "string" } } + }, + { + "required": ["enum"], + "properties": { + "enum": { + "type": "array", + "contains": { "type": "string" }, + "minContains": 1 + }, + } + } + ] + } + }); + + JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&my_schema) + .expect("Error encountered: Failed to compile 'context_dimension_schema_value'. Ensure it adheres to the correct format and data type.") +} + +pub fn validate_context_jsonschema( + object_key: &str, + dimension_value: &Value, + dimension_schema: &JSONSchema, +) -> superposition::Result<()> { + match dimension_value { + Value::Array(val_arr) if object_key == "in" => { + let mut verrors = Vec::new(); + val_arr.into_iter().for_each(|x| { + dimension_schema + .validate(&x) + .map_err(|e| { + verrors.append(&mut e.collect::>()); + }) + .ok(); + }); + if verrors.is_empty() { + Ok(()) + } else { + // Check if the array as a whole validates, even with individual errors + match dimension_schema.validate(&dimension_value) { + Ok(()) => { + log::error!( + "Validation errors for individual dimensions, but array as a whole validates: {:?}", + verrors + ); + Ok(()) + } + Err(e) => { + verrors.append(&mut e.collect::>()); + log::error!( + "Validation errors for dimensions in array: {:?}", + verrors + ); + Err(validation_error!( + "failed to validate dimension value {:?} with error: {:?}", + dimension_value, + verrors + )) + } + } + } + } + _ => dimension_schema.validate(dimension_value).map_err(|e| { + let verrors = e.collect::>(); + log::error!( + "failed to validate dimension value {:?} with error : {:?}", + dimension_value, + verrors + ); + validation_error!( + "failed to validate dimension value {:?} with error: {:?}", + dimension_value, + verrors + ) + }), + } +} + +/* + This step is required because an empty object + is also a valid JSON schema. So added required + validations for the input. +*/ +// TODO: Recursive validation. + +pub fn validate_jsonschema( + validation_schema: &JSONSchema, + schema: &Value, +) -> superposition::Result<()> { + let res = match validation_schema.validate(schema) { + Ok(_) => Ok(()), + Err(e) => { + //TODO: Try & render as json. + let verrors = e.collect::>(); + Err(validation_error!( + "schema validation failed: {:?}", + verrors.as_slice() + )) + } + }; + res +} + +pub fn json_to_sorted_string(v: &Value) -> String { + match v { + Value::Object(m) => { + let mut new_str: String = String::from(""); + for (i, val) in m.iter().sorted_by_key(|item| item.0) { + let p: String = json_to_sorted_string(val); + new_str.push_str(i); + new_str.push_str(&String::from(":")); + new_str.push_str(&p); + new_str.push_str(&String::from("$")); + } + new_str + } + Value::String(m) => m.to_string(), + Value::Number(m) => m.to_string(), + Value::Bool(m) => m.to_string(), + Value::Null => String::from("null"), + Value::Array(m) => { + let mut new_vec: Vec = m + .iter() + .map(|item| json_to_sorted_string(item)) + .collect::>(); + new_vec.sort(); + new_vec.join(",") + } + } +} + +// ************ Tests ************* + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_get_meta_schema() { + let x = get_meta_schema(); + let ok_string_validation = x + .validate(&json!({"type": "string", "pattern": ".*"})) + .map_err(|e| { + let verrors = e.collect::>(); + String::from(format!("Bad schema: {:?}", verrors.as_slice())) + }); + let error_string_validation = + match x.validate(&json!({"type": "string"})).map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating string dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }) { + Ok(()) => false, + Err(err_str) => err_str.contains("Bad schema"), + }; + + let error_object_validation = + match x.validate(&json!({"type": "object"})).map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating object dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }) { + Ok(()) => false, + Err(err_str) => err_str.contains("Bad schema"), + }; + let ok_enum_validation = x + .validate(&json!({"type": "string", "enum": ["ENUMVAL"]})) + .map_err(|e| { + let verrors = e.collect::>(); + String::from(format!( + "Error While validating enum dataType, Bad schema: {:?}", + verrors.as_slice() + )) + }); + assert_eq!(ok_enum_validation, Ok(())); + assert_eq!(error_object_validation, true); + assert_eq!(ok_string_validation, Ok(())); + assert_eq!(error_string_validation, true); + } + + #[test] + fn test_json_to_sorted_string() { + let first_condition: Value = json!({ + "and": [ + { + "==": [ + { + "var": "os" + }, + "android" + ] + }, + { + "==": [ + { + "var": "clientId" + }, + "geddit" + ] + } + ] + }); + + let second_condition: Value = json!({ + "and": [ + { + "==": [ + { + "var": "clientId" + }, + "geddit" + ] + }, + { + "==": [ + { + "var": "os" + }, + "android" + ] + } + ] + }); + let expected_string: String = + "and:==:android,var:os$$,==:geddit,var:clientId$$$".to_owned(); + assert_eq!(json_to_sorted_string(&first_condition), expected_string); + assert_eq!( + json_to_sorted_string(&first_condition), + json_to_sorted_string(&second_condition) + ); + } + + #[test] + fn test_validate_context_jsonschema() { + let test_schema = json!({ + "type": "string", + "pattern": ".*" + }); + let test_jsonschema = JSONSchema::options() + .with_draft(Draft::Draft7) + .compile(&test_schema) + .expect("Error encountered: Failed to compile 'context_dimension_schema_value'. Ensure it adheres to the correct format and data type."); + + let str_dimension_val = json!("string1".to_owned()); + let arr_dimension_val = json!(["string1".to_owned(), "string2".to_owned()]); + let ok_str_context = + validate_context_jsonschema("in", &str_dimension_val, &test_jsonschema); + let ok_arr_context = + validate_context_jsonschema("in", &arr_dimension_val, &test_jsonschema); + let err_arr_context = + match validate_context_jsonschema("==", &arr_dimension_val, &test_jsonschema) + { + Err(superposition::AppError::ValidationError(err)) => { + log::info!("{:?}", err); + true + } + _ => false, + }; + + assert_eq!(ok_str_context.unwrap(), ()); + assert_eq!(err_arr_context, true); + assert_eq!(ok_arr_context.unwrap(), ()); + } +} diff --git a/crates/context_aware_config/src/lib.rs b/crates/context_aware_config/src/lib.rs new file mode 100644 index 000000000..c3971d3a1 --- /dev/null +++ b/crates/context_aware_config/src/lib.rs @@ -0,0 +1,5 @@ +pub mod api; +pub mod db; +pub mod helpers; +pub mod middlewares; +pub mod validation_functions; diff --git a/crates/context_aware_config/src/middlewares/cookie_to_header.rs b/crates/context_aware_config/src/middlewares/cookie_to_header.rs new file mode 100644 index 000000000..da4d44a64 --- /dev/null +++ b/crates/context_aware_config/src/middlewares/cookie_to_header.rs @@ -0,0 +1,62 @@ +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::header::{HeaderName, HeaderValue}, + Error, +}; +use futures_util::future::LocalBoxFuture; +use std::future::{ready, Ready}; +use std::rc::Rc; + +pub struct CookieToHeader; + +impl Transform for CookieToHeader +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = CookieToHeaderMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(CookieToHeaderMiddleware { + service: Rc::new(service), + // other fields if required + })) + } +} + +pub struct CookieToHeaderMiddleware { + service: Rc, +} + +impl Service for CookieToHeaderMiddleware +where + S: Service, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); + Box::pin(async move { + if let Some(cookie) = req.cookie("token") { + let token_value = cookie.value().to_string(); + req.headers_mut().insert( + HeaderName::from_static("authorization"), + HeaderValue::from_str(&format!("Bearer {}", token_value)) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), + ); + } + srv.call(req).await + }) + } +} diff --git a/crates/context_aware_config/src/middlewares/mod.rs b/crates/context_aware_config/src/middlewares/mod.rs new file mode 100644 index 000000000..842b49c6b --- /dev/null +++ b/crates/context_aware_config/src/middlewares/mod.rs @@ -0,0 +1 @@ +pub mod cookie_to_header; diff --git a/crates/context_aware_config/src/validation_functions.rs b/crates/context_aware_config/src/validation_functions.rs new file mode 100644 index 000000000..3f0f7b417 --- /dev/null +++ b/crates/context_aware_config/src/validation_functions.rs @@ -0,0 +1,170 @@ +use serde_json::{json, Value}; +use service_utils::result as superposition; +use service_utils::unexpected_error; +use service_utils::validation_error; +use std::process::Command; +use std::str; + +fn type_check_validate(code_str: &str) -> String { + format!( + r#"const vm = require("node:vm") + const axios = require("./target/node_modules/axios") + const script = new vm.Script(\` + + {} + + if(typeof(validate)!="function") + {{ + throw Error("validate is not of function type") + }}\`); + + script.runInNewContext({{axios,console}}, {{ timeout: 1500}}); + "#, + code_str + ) +} + +fn execute_validate_fun(code_str: &str, val: Value) -> String { + format!( + r#" + const vm = require("node:vm") + const axios = require("./target/node_modules/axios") + const script = new vm.Script(\` + + {} + Promise.resolve(validate({})).then((output) => {{ + + if(output!=true){{ + throw new Error("The function did not return true as expected. Check the conditions or logic inside the function.") + }} + return output; + }}).catch((err)=> {{ + throw new Error(err) + }});\`); + + script.runInNewContext({{axios,console,process}}, {{ timeout: 1500}}); + "#, + code_str, val + ) +} + +fn generate_code(code_str: &str) -> String { + format!( + r#" + const {{ Worker, isMainThread, threadId }} = require("node:worker_threads"); + + if (isMainThread) {{ + + // starting worker thread , making separated from the main thread + function runService() {{ + return new Promise((resolve, reject) => {{ + const worker = new Worker( + `{}`,{{eval:true}} + ); + worker.on("message", (msg) => {{ + console.log(msg); + }}); + worker.on("error", (err) => {{ + clearTimeout(tl); + console.error(err.message); + process.exit(1); + }}); + worker.on("exit", (code) => {{ + clearTimeout(tl); + if (code !== 0) {{ + console.error(`Script stopped with exit code ${{code}}`); + process.exit(code); + }} else {{ + worker.terminate(); + }} + }}); + + function timelimit() {{ + worker.terminate(); + throw new Error("time limit exceeded"); + }} + + // terminate worker thread if execution time exceed 2 secs + + var tl = setTimeout(timelimit, 2000); + }}); + }} + + async function run() {{ + const result = await runService(); + console.log("result output: ", result); + }} + run().catch((err) => console.error(err)); + }} + + "#, + code_str + ) +} + +pub fn execute_fn( + code_str: &str, + key: &str, + value: Value, +) -> Result)> { + let fun_val = json!({ + "key": key, + "value": value + }); + let exec_code = execute_validate_fun(code_str, fun_val); + let output = Command::new("node") + .arg("-e") + .arg(generate_code(&exec_code)) + .output(); + log::trace!("{}", format!("validation function output : {:?}", output)); + match output { + Ok(val) => { + let stdout = str::from_utf8(&val.stdout) + .unwrap_or("[Invalid UTF-8 in stdout]") + .to_owned(); + if !(val.status.success()) { + let stderr = str::from_utf8(&val.stderr) + .unwrap_or("[Invalid UTF-8 in stderr]") + .to_owned(); + log::error!( + "{}", + format!("validation function output error: {:?}", stderr) + ); + Err((stderr, Some(stdout))) + } else { + Ok(stdout) + } + } + Err(e) => { + log::error!("js_eval error: {}", e); + Err((format!("js_eval error: {}", e), None)) + } + } +} + +pub fn compile_fn(code_str: &str) -> superposition::Result<()> { + let type_check_code = type_check_validate(code_str); + let output = Command::new("node") + .arg("-e") + .arg(generate_code(&type_check_code)) + .output(); + + log::trace!("{}", format!("validation function output : {:?}", output)); + match output { + Ok(val) => { + if !(val.status.success()) { + let stderr = str::from_utf8(&val.stderr) + .unwrap_or("[Invalid UTF-8 in stderr]") + .to_owned(); + log::error!("{}", format!("eslint check output error: {:?}", stderr)); + Err(validation_error!(stderr)) + } else { + Ok(()) + } + } + Err(e) => { + log::error!("eslint check error: {}", e); + Err(unexpected_error!("js_eval error: {}", e)) + } + } +} diff --git a/crates/context_aware_config/tests/cac_tests.rs b/crates/context_aware_config/tests/cac_tests.rs new file mode 100644 index 000000000..cd812a1dd --- /dev/null +++ b/crates/context_aware_config/tests/cac_tests.rs @@ -0,0 +1,42 @@ +use context_aware_config::validation_functions::{compile_fn, execute_fn}; +use serde_json::json; +use service_utils::result as superposition; + +// #[test] //todo : currently there is issue in running this test +fn test_execute_fn() { + let code_ok = r#" + function validate() { + return true; + }; + "#; + + let execute_code_error = r#" + function validate() { + return false; + } + "#; + + let compile_code_error = r#" + function validate( { + return true; + } + "#; + + let err_execute = + match execute_fn(&(execute_code_error.to_owned()), "test", json!(10)) { + Ok(_) => false, + Err((e, _)) => e.contains("Bad schema"), + }; + let err_compile = match compile_fn(&(compile_code_error.to_owned())) { + Ok(()) => false, + Err(superposition::AppError::ValidationError(_)) => true, + _ => false, + }; + assert_eq!( + execute_fn(&(code_ok.to_owned()), "test", json!(10)), + Ok("true".to_string()) + ); + assert_eq!(err_execute, true); + assert_eq!(compile_fn(&(code_ok.to_owned())).unwrap(), ()); + assert_eq!(err_compile, true); +} diff --git a/test/index.js b/crates/context_aware_config/tests/index.js similarity index 100% rename from test/index.js rename to crates/context_aware_config/tests/index.js diff --git a/crates/experimentation_client/CHANGELOG.md b/crates/experimentation_client/CHANGELOG.md new file mode 100644 index 000000000..9d21a2949 --- /dev/null +++ b/crates/experimentation_client/CHANGELOG.md @@ -0,0 +1,66 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## experimentation_client-v0.5.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + +## experimentation_client-v0.4.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## experimentation_client-v0.3.0 - 2023-10-27 +#### Features +- multi-tenant support for client libraries - (c603be0) - Shubhranshu Sanjeev + +- - - + +## experimentation_client-v0.2.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + +## experimentation_client-v0.1.3 - 2023-10-13 +#### Bug Fixes +- PICAF-24612 add all variants in manifest - (0f15ac9) - Pratik Mishra + +- - - + +## experimentation_client-v0.1.2 - 2023-09-06 +#### Bug Fixes +- trimming newline character from version string - (2c61077) - Shubhranshu Sanjeev + +- - - + +## experimentation_client-v0.1.1 - 2023-09-06 +#### Bug Fixes +- fixed setting env in docker image - (272454b) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-24114 updated integ AP tracker curl with new version - (1e0fa5b) - Ritick Madaan + +- - - + +## experimentation_client-v0.1.0 - 2023-09-01 +#### Bug Fixes +- PICAF-24114 removed unwanted parameter to prevent warning - (3de7fe7) - Ritick Madaan +- PICAF-24114 allowing cug users to fall under test variants - (c095333) - Ritick Madaan +- [PICAF-23846] added total items to list API response - (17955fa) - Kartik Gajendra +- PICAF-23632 - (247542e) - Ritick Madaan +- [PICAF-23632] minor fixes for exp client - (64deee5) - Kartik Gajendra +#### Features +- [PICAF-24010] added support for CUG in super position client - (4eeae99) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/experimentation_client/Cargo.toml b/crates/experimentation_client/Cargo.toml new file mode 100644 index 000000000..4e0e8f819 --- /dev/null +++ b/crates/experimentation_client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "experimentation_client" +version = "0.5.0" +edition = "2021" + +[dependencies] +once_cell = { workspace = true } +chrono = { workspace = true } +jsonlogic = { workspace = true } +reqwest = { workspace = true , features = ["json"]} +serde = { workspace = true } +serde_json = { workspace = true } +tokio = {version = "1.29.1", features = ["full"]} +dotenv = { workspace = true } +derive_more = { workspace = true } +log = { workspace = true } + +[lib] +name = "experimentation_client" +crate-type = ["cdylib", "lib"] + +[build-dependencies] +cbindgen = "0.26.0" diff --git a/crates/experimentation_client/README.md b/crates/experimentation_client/README.md new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/crates/experimentation_client/README.md @@ -0,0 +1 @@ + diff --git a/crates/experimentation_client/build.rs b/crates/experimentation_client/build.rs new file mode 100644 index 000000000..2530a7a98 --- /dev/null +++ b/crates/experimentation_client/build.rs @@ -0,0 +1,10 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut config: cbindgen::Config = Default::default(); + config.language = cbindgen::Language::C; + cbindgen::generate_with_config(&crate_dir, config) + .unwrap() + .write_to_file("../../headers/libexperimentation_client.h"); +} diff --git a/crates/experimentation_client/src/interface.rs b/crates/experimentation_client/src/interface.rs new file mode 100644 index 000000000..3ec05345a --- /dev/null +++ b/crates/experimentation_client/src/interface.rs @@ -0,0 +1,238 @@ +use std::{ + ffi::{c_char, c_ulong, CStr}, + sync::Arc, +}; + +use crate::{Client, CLIENT_FACTORY}; +use serde_json::Value; +use std::{ + cell::RefCell, + ffi::{c_int, c_short, CString}, +}; +use tokio::{runtime::Runtime, task}; + +thread_local! { + static LAST_ERROR: RefCell> = RefCell::new(None); +} + +fn to_string(e: E) -> String +where + E: ToString, +{ + e.to_string() +} + +fn error_block(err: String) -> *mut E { + update_last_error(err); + std::ptr::null_mut() +} + +fn cstring_to_rstring(s: *const c_char) -> Result { + let s = unsafe { CStr::from_ptr(s) }; + s.to_str().map(str::to_string).map_err(to_string) +} + +fn rstring_to_cstring(s: String) -> CString { + CString::new(s.as_str()).unwrap_or_default() +} + +pub fn update_last_error(err: String) { + println!("Setting LAST_ERROR: {}", err); + + LAST_ERROR.with(|prev| { + *prev.borrow_mut() = Some(err); + }); +} + +pub fn take_last_error() -> Option { + LAST_ERROR.with(|prev| prev.take()) +} + +#[no_mangle] +pub extern "C" fn last_error_length() -> c_int { + LAST_ERROR.with(|prev| match *prev.borrow() { + Some(ref err) => err.to_string().len() as c_int + 1, + None => 0, + }) +} + +#[no_mangle] +pub unsafe extern "C" fn last_error_message() -> *const c_char { + let last_error = match take_last_error() { + Some(err) => err, + None => return std::ptr::null_mut(), + }; + let error_message = last_error.to_string(); + // println!("Error in last_error_message {error_message}"); + let err = rstring_to_cstring(error_message); + err.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn free_string(s: *mut c_char) { + if s.is_null() { + return; + } + unsafe { + let _ = CString::from_raw(s); + } +} + +#[no_mangle] +pub extern "C" fn new_client( + tenant: *const c_char, + update_frequency: c_ulong, + hostname: *const c_char, +) -> c_int { + let tenant = match cstring_to_rstring(tenant) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + let hostname = match cstring_to_rstring(hostname) { + Ok(value) => value, + Err(err) => { + update_last_error(err); + return 1; + } + }; + + // println!("Creating cac client thread for tenant {tenant}"); + let local = task::LocalSet::new(); + local.block_on(&Runtime::new().unwrap(), async move { + match CLIENT_FACTORY + .create_client(tenant.clone(), update_frequency, hostname) + .await + { + Ok(_) => 0, + Err(err) => { + update_last_error(err); + 1 + } + } + }); + 0 +} + +#[no_mangle] +pub extern "C" fn start_polling_update(tenant: *const c_char) { + if tenant.is_null() { + return (); + } + unsafe { + let client = get_client(tenant); + let local = task::LocalSet::new(); + // println!("in FFI polling"); + local.block_on( + &Runtime::new().unwrap(), + (*client).clone().run_polling_updates(), + ); + } +} + +#[no_mangle] +pub extern "C" fn free_client(ptr: *mut Arc) { + if ptr.is_null() { + return; + } + unsafe { + let _ = Box::from_raw(ptr); + } +} + +#[no_mangle] +pub extern "C" fn get_client(tenant: *const c_char) -> *mut Arc { + let ten = match cstring_to_rstring(tenant) { + Ok(t) => t, + Err(err) => { + update_last_error(err); + return std::ptr::null_mut(); + } + }; + let local = task::LocalSet::new(); + local.block_on( + &Runtime::new().unwrap(), + // println!("fetching exp client thread for tenant {ten}"); + async move { + match CLIENT_FACTORY.get_client(ten).await { + Ok(client) => Box::into_raw(Box::new(client)), + Err(err) => { + // println!("error occurred {err}"); + update_last_error(err); + // println!("error set"); + std::ptr::null_mut() + } + } + }, + ) +} + +#[no_mangle] +pub extern "C" fn get_applicable_variant( + client: *mut Arc, + c_context: *const c_char, + toss: c_short, +) -> *mut c_char { + let context = match cstring_to_rstring(c_context) { + Ok(c) => match serde_json::from_str::(c.as_str()) { + Ok(con) => con, + Err(err) => return error_block(err.to_string()), + }, + Err(err) => return error_block(err), + }; + // println!("Fetching variantIds"); + let local = task::LocalSet::new(); + let variants = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_applicable_variant(&context, toss as i8) + }); + // println!("variantIds: {:?}", variants); + match serde_json::to_string::>(&variants) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => error_block(err.to_string()), + } +} + +#[no_mangle] +pub extern "C" fn get_satisfied_experiments( + client: *mut Arc, + c_context: *const c_char, +) -> *mut c_char { + let context = match cstring_to_rstring(c_context) { + Ok(c) => match serde_json::from_str::(c.as_str()) { + Ok(con) => con, + Err(err) => return error_block(err.to_string()), + }, + Err(err) => return error_block(err), + }; + + let local = task::LocalSet::new(); + let experiments = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_satisfied_experiments(&context) + }); + let experiments = match serde_json::to_value(experiments) { + Ok(value) => value, + Err(err) => return error_block(err.to_string()), + }; + match serde_json::to_string(&experiments) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => error_block(err.to_string()), + } +} + +#[no_mangle] +pub extern "C" fn get_running_experiments(client: *mut Arc) -> *mut c_char { + let local = task::LocalSet::new(); + let experiments = local.block_on(&Runtime::new().unwrap(), unsafe { + (*client).get_running_experiments() + }); + let experiments = match serde_json::to_value(experiments) { + Ok(value) => value, + Err(err) => return error_block(err.to_string()), + }; + match serde_json::to_string(&experiments) { + Ok(result) => rstring_to_cstring(result).into_raw(), + Err(err) => error_block(err.to_string()), + } +} diff --git a/crates/experimentation_client/src/lib.rs b/crates/experimentation_client/src/lib.rs new file mode 100644 index 000000000..a71143021 --- /dev/null +++ b/crates/experimentation_client/src/lib.rs @@ -0,0 +1,205 @@ +mod interface; +mod types; +use std::{collections::HashMap, sync::Arc}; + +use chrono::{DateTime, TimeZone, Utc}; +use derive_more::{Deref, DerefMut}; +use serde_json::Value; +use tokio::{ + sync::RwLock, + time::{self, Duration}, +}; +pub use types::{Config, Experiment, Experiments, Variants}; +use types::{ExperimentStore, ListExperimentsResponse, Variant, VariantType}; + +#[derive(Clone, Debug)] +pub struct Client { + pub client_config: Arc, + pub(crate) experiments: Arc>, + pub(crate) http_client: reqwest::Client, + last_polled: Arc>>, +} + +//TODO: replace all unwraps with proper error handling +// DO NOT let panics show up in library + +impl Client { + pub fn new(config: Config) -> Self { + Client { + client_config: Arc::new(config), + experiments: Arc::new(RwLock::new(HashMap::new())), + http_client: reqwest::Client::new(), + last_polled: Arc::new(RwLock::new( + Utc.with_ymd_and_hms(2023, 01, 1, 0, 0, 0).unwrap(), + )), + } + } + + pub async fn run_polling_updates(self: Arc) { + let poll_interval = self.client_config.poll_frequency; + let hostname = &self.client_config.hostname; + let mut interval = time::interval(Duration::from_secs(poll_interval)); + let mut start_date = self.last_polled.write().await; + loop { + // NOTE: this additional block scopes the write lock + // at the end of this block, the write lock on exp store is released + // allowing other threads to read updated data + { + let experiments = get_experiments( + hostname.clone(), + self.http_client.clone(), + start_date.to_string(), + self.client_config.tenant.to_string(), + ) + .await + .unwrap(); + + 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) + } + _ => exp_store.insert(exp_id, experiment), + }; + } + } // write lock on exp store releases here + *start_date = Utc::now(); + interval.tick().await; + } + } + + pub async fn get_applicable_variant(&self, context: &Value, toss: i8) -> Vec { + let experiments: Experiments = self.get_satisfied_experiments(context).await; + let mut variants: Vec = Vec::new(); + for exp in experiments { + if let Some(v) = + self.decide_variant(exp.traffic_percentage, exp.variants, toss) + { + variants.push(v.id) + } + } + variants + } + + pub async fn get_satisfied_experiments(&self, context: &Value) -> Experiments { + let running_experiments = self.experiments.read().await; + running_experiments + .iter() + .filter(|(_, exp)| { + jsonlogic::apply(&exp.context, context) == Ok(Value::Bool(true)) + }) + .map(|(_, exp)| exp.clone()) + .collect::() + } + + pub async fn get_running_experiments(&self) -> Experiments { + let running_experiments = self.experiments.read().await; + let experiments: Experiments = running_experiments.values().cloned().collect(); + experiments + } + + // decide which variant to return among all applicable experiments + fn decide_variant( + &self, + traffic: u8, + applicable_variants: Variants, + toss: i8, + ) -> Option { + if toss < 0 { + for variant in applicable_variants.iter() { + if variant.variant_type == VariantType::EXPERIMENTAL { + return Some(variant.clone()); + } + } + } + let variant_count = applicable_variants.len() as u8; + let range = (traffic * variant_count) as i32; + if (toss as i32) >= range { + return None; + } + let buckets = (1..=variant_count) + .map(|i| (traffic * i) as i8) + .collect::>(); + let index = buckets.into_iter().position(|x| toss < x); + applicable_variants.get(index.unwrap()).map(Variant::clone) + } +} + +async fn get_experiments( + hostname: String, + http_client: reqwest::Client, + start_date: String, + tenant: String, +) -> Result { + let mut curr_exp_store: ExperimentStore = HashMap::new(); + let requesting_count = 10; + let mut page = 1; + let now = Utc::now(); + loop { + let endpoint = format!( + "{hostname}/experiments?from_date={start_date}&to_date={now}&page={page}&count={requesting_count}" + ); + let list_experiments_response = http_client + .get(format!("{endpoint}&status=CREATED,INPROGRESS,CONCLUDED")) + .header("x-tenant", tenant.to_string()) + .send() + .await + .unwrap() + .json::() + .await + .unwrap_or_default(); + + let experiments = list_experiments_response.data; + // println!("got these running experiments: {:?}", running_experiments); + + for experiment in experiments.into_iter() { + curr_exp_store.insert(experiment.id.to_string(), experiment); + } + if page < list_experiments_response.total_pages { + page += 1; + } else { + break; + } + } + + Ok(curr_exp_store) +} + +#[derive(Deref, DerefMut)] +pub struct ClientFactory(RwLock>>); +impl ClientFactory { + pub async fn create_client( + &self, + tenant: String, + poll_frequency: u64, + hostname: String, + ) -> Result, String> { + let mut factory = self.write().await; + + if let Some(client) = factory.get(&tenant) { + return Ok(client.clone()); + } + + let client = Arc::new(Client::new(Config { + tenant: tenant.to_string(), + hostname, + poll_frequency, + })); + + factory.insert(tenant.to_string(), client.clone()); + Ok(client.clone()) + } + + pub async fn get_client(&self, tenant: String) -> Result, String> { + let factory = self.read().await; + match factory.get(&tenant) { + Some(client) => Ok(client.clone()), + None => Err("No such tenant found".to_string()), + } + } +} + +use once_cell::sync::Lazy; +pub static CLIENT_FACTORY: Lazy = + Lazy::new(|| ClientFactory(RwLock::new(HashMap::new()))); diff --git a/crates/experimentation_client/src/types.rs b/crates/experimentation_client/src/types.rs new file mode 100644 index 000000000..b12b9cef3 --- /dev/null +++ b/crates/experimentation_client/src/types.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Clone, Debug)] +pub struct Config { + pub tenant: String, + pub hostname: String, + pub poll_frequency: u64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub(crate) enum ExperimentStatusType { + CREATED, + INPROGRESS, + CONCLUDED, +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub(crate) enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[repr(C)] +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub overrides: Value, + pub(crate) variant_type: VariantType, +} + +pub type Variants = Vec; + +#[repr(C)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Experiment { + pub variants: Variants, + pub(crate) name: String, + pub(crate) id: String, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, +} + +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_client_integration_example/Cargo.toml b/crates/experimentation_client_integration_example/Cargo.toml new file mode 100644 index 000000000..cb5938bb3 --- /dev/null +++ b/crates/experimentation_client_integration_example/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +experimentation_client = { path = "../experimentation_client" } +chrono = { workspace = true } + +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/experimentation_client_integration_example/src/main.rs b/crates/experimentation_client_integration_example/src/main.rs new file mode 100644 index 000000000..f071a3ed0 --- /dev/null +++ b/crates/experimentation_client_integration_example/src/main.rs @@ -0,0 +1,47 @@ +use actix_web::{ + get, rt, + web::{get, Data, Path}, + App, HttpResponse, HttpServer, +}; + +use experimentation_client as exp; +use serde_json::json; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let client_configuration = exp::Config { + tenant: "tenant".to_string(), + hostname: "http://localhost:8080".to_string(), + poll_frequency: 10, + }; + let client = std::sync::Arc::new(exp::Client::new(client_configuration)); + rt::spawn(client.clone().run_polling_updates()); + HttpServer::new(move || { + App::new() + .app_data(Data::new(client.clone())) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + .service(get_variants) + }) + .bind(("127.0.0.1", 8083))? + .run() + .await +} + +#[get("/variants/{client_id}/{platform}/{toss}")] +async fn get_variants( + state: Data, + path: Path<(String, String, i8)>, +) -> HttpResponse { + let (client_id, platform, toss) = path.into_inner(); + println!("client state on the server = {:?}", state); + let contexts = json!({ + "clientId": client_id, + "os": platform + }); + let variant = state.get_applicable_variant(&contexts, toss).await; + println!("variant value: {:?}", variant); + HttpResponse::Ok().body("check your console") +} diff --git a/crates/experimentation_platform/CHANGELOG.md b/crates/experimentation_platform/CHANGELOG.md new file mode 100644 index 000000000..b66930d51 --- /dev/null +++ b/crates/experimentation_platform/CHANGELOG.md @@ -0,0 +1,164 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## experimentation-platform-v0.12.0 - 2024-04-05 +#### Documentation +- PICAF-25981: add intro doc and features - (64fa30f) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (651a66d) - Kartik + +- - - + +## experimentation-platform-v0.11.0 - 2024-03-18 +#### Documentation +- PICAF-25981: add intro doc and features - (d09ba53) - Natarajan Kannan +#### Features +- [PICAF-26126] haskell client for superposition - (7106b56) - Kartik + +- - - + +## experimentation-platform-v0.10.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + +## experimentation-platform-v0.9.4 - 2024-02-27 +#### Bug Fixes +- returning error response if CAC call not 200 - (fa0eb5e) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.9.3 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + +## experimentation-platform-v0.9.2 - 2024-01-29 +#### Bug Fixes +- added partitions for audit_log table in cac schema - (d771050) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.9.1 - 2024-01-22 +#### Bug Fixes +- added partitions for 2025 and 2026 for audit table - (45d37dd) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.9.0 - 2024-01-04 +#### Bug Fixes +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +#### Features +- working resolve page - (803dfbd) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.8.2 - 2023-11-30 +#### Bug Fixes +- allow ramp 0 - (b8d49aa) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.8.1 - 2023-11-16 +#### Bug Fixes +- add different auth types for exp requests to CAC - (bd8ae88) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.8.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.7.1 - 2023-11-09 +#### Bug Fixes +- Removing acceptance of override_keys in experiment create/update - (033597e) - ankit.mahato + +- - - + +## experimentation-platform-v0.7.0 - 2023-11-08 +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.6.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev +#### Refactoring +- moved tables and types out of cac_v1 schema - (f70a0c5) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.5.0 - 2023-10-10 +#### Bug Fixes +- validating override_keys for unique entries - (36cf523) - Shubhranshu Sanjeev +#### Features +- support to update experiment override_keys and variants - (9432bf7) - Shubhranshu Sanjeev +#### Refactoring +- resolved comments - (aefb03e) - Shubhranshu Sanjeev +#### Tests +- added tests for experiment helper fnxs - (ea4db17) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.4.0 - 2023-10-05 +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.3.1 - 2023-09-12 +#### Bug Fixes +- failed build due to untracked schema.rs file changes - (5bc4eae) - Shubhranshu Sanjeev +- fixed random timeouts in internal http calls to CAC - (a4e95a3) - Shubhranshu Sanjeev + +- - - + +## experimentation-platform-v0.3.0 - 2023-09-06 +#### Features +- [PICAF-24160] record the chosen variant after conclude - (1c3c6e6) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.2.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra + +- - - + +## experimentation-platform-v0.1.0 - 2023-09-01 +#### Bug Fixes +- using audit log tstamp for checking last-modified - (2ccaa7e) - Shubhranshu Sanjeev +- [PICAF-23846] added total items to list API response - (17955fa) - Kartik Gajendra +- removed traffic-percentage from experiment create request - (2a62555) - Shubhranshu Sanjeev +- PICAF-23632 - (247542e) - Ritick Madaan +- PICAF-23622 updated last_modified in ramp - (fcbaaa4) - ankit.mahato +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +- moved tables and types under cac_v1 schema - (1be82f1) - Shubhranshu Sanjeev +- added last_modified column and indexes - (942d723) - Shubhranshu Sanjeev +- fixed context overlap check logic - (691eae7) - Shubhranshu Sanjeev +#### Continuous Integration +- regenerated schema.patch with latest schema.rs - (390818a) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (91386ee) - Kartik Gajendra +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- added log table for all cac_v1 tables - (88a3328) - Shubhranshu Sanjeev +- [PICAF-23856] add support for last - (d23ee26) - Kartik Gajendra +- [PICAF-23632] added experimentation client with few fixes - (9a31815) - Kartik Gajendra +- added conclude functionality for experiments - (4def4bc) - Shubhranshu Sanjeev +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +#### Refactoring +- improvements to APIs - (60bf5c0) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/experimentation_platform/Cargo.toml b/crates/experimentation_platform/Cargo.toml new file mode 100644 index 000000000..31597c740 --- /dev/null +++ b/crates/experimentation_platform/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "experimentation_platform" +version = "0.12.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +service_utils = { path = "../service_utils" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } +reqwest = { workspace = true } +anyhow = { workspace = true } \ No newline at end of file diff --git a/crates/experimentation_platform/diesel.toml b/crates/experimentation_platform/diesel.toml new file mode 100644 index 000000000..4f544dd62 --- /dev/null +++ b/crates/experimentation_platform/diesel.toml @@ -0,0 +1,6 @@ +[print_schema] +file = "src/db/schema.rs" +patch_file = "src/schema.patch" + +[migrations_directory] +dir = "migrations" \ No newline at end of file diff --git a/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 000000000..a9f526091 --- /dev/null +++ b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- 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 new file mode 100644 index 000000000..d68895b1a --- /dev/null +++ b/crates/experimentation_platform/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- 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/down.sql b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/down.sql new file mode 100644 index 000000000..291a97c5c --- /dev/null +++ b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file 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 new file mode 100644 index 000000000..f44d33025 --- /dev/null +++ b/crates/experimentation_platform/migrations/2023-10-16-134612_experimentation-init/up.sql @@ -0,0 +1,184 @@ +-- 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(); \ No newline at end of file diff --git a/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql new file mode 100644 index 000000000..d9a93fe9a --- /dev/null +++ b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` diff --git a/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql new file mode 100644 index 000000000..dbd5c77b2 --- /dev/null +++ b/crates/experimentation_platform/migrations/2024-01-18-063937_audit_log_partitions/up.sql @@ -0,0 +1,118 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS public.event_log_y2024m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-08-01') TO ('2024-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-09-01') TO ('2024-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-10-01') TO ('2024-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-11-01') TO ('2024-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2024m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2024-12-01') TO ('2025-01-01'); + +------------ Parititions for 2025 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2025m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-01-01') TO ('2025-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-02-01') TO ('2025-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-03-01') TO ('2025-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-04-01') TO ('2025-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-05-01') TO ('2025-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-06-01') TO ('2025-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-07-01') TO ('2025-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-08-01') TO ('2025-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-09-01') TO ('2025-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-10-01') TO ('2025-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-11-01') TO ('2025-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2025m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2025-12-01') TO ('2026-01-01'); + +------------ Parititions for 2026 ----------- +CREATE TABLE IF NOT EXISTS public.event_log_y2026m01 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-01-01') TO ('2026-02-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m02 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-02-01') TO ('2026-03-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m03 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-03-01') TO ('2026-04-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m04 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-04-01') TO ('2026-05-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m05 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-05-01') TO ('2026-06-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m06 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-06-01') TO ('2026-07-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m07 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-07-01') TO ('2026-08-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m08 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-08-01') TO ('2026-09-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m09 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-09-01') TO ('2026-10-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m10 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-10-01') TO ('2026-11-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m11 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-11-01') TO ('2026-12-01'); + +CREATE TABLE IF NOT EXISTS public.event_log_y2026m12 PARTITION OF public.event_log FOR +VALUES +FROM ('2026-12-01') TO ('2027-01-01'); \ No newline at end of file diff --git a/crates/experimentation_platform/src/api/experiments/handlers.rs b/crates/experimentation_platform/src/api/experiments/handlers.rs new file mode 100644 index 000000000..df77dfb93 --- /dev/null +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -0,0 +1,753 @@ +use std::collections::{HashMap, HashSet}; + +use actix_web::{ + get, patch, post, put, + web::{self, Data, Json, Query}, + HttpRequest, HttpResponse, Scope, +}; +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; +use diesel::{ + r2d2::{ConnectionManager, PooledConnection}, + ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl, +}; + +use service_utils::{ + bad_argument, response_error, result as superposition, unexpected_error, +}; + +use superposition_types::{SuperpositionUser, User}; + +use reqwest::{Response, StatusCode}; +use service_utils::service::types::{AppState, DbConnection, Tenant}; + +use super::{ + helpers::{ + add_variant_dimension_to_ctx, check_variant_types, + check_variants_override_coverage, extract_override_keys, validate_experiment, + validate_override_keys, + }, + types::{ + AuditQueryFilters, ConcludeExperimentRequest, ContextAction, ContextBulkResponse, + ContextMoveReq, ContextPutReq, ExperimentCreateRequest, ExperimentCreateResponse, + ExperimentResponse, ExperimentsResponse, ListFilters, OverrideKeysUpdateRequest, + RampRequest, Variant, + }, +}; + +use crate::{ + db::models::{EventLog, Experiment, ExperimentStatusType}, + db::schema::{event_log::dsl as event_log, experiments::dsl as experiments}, +}; + +use serde_json::{json, Map, Value}; + +pub fn endpoints(scope: Scope) -> Scope { + scope + .service(get_audit_logs) + .service(create) + .service(conclude_handler) + .service(list_experiments) + .service(get_experiment_handler) + .service(ramp) + .service(update_overrides) +} + +async fn parse_error_response( + response: reqwest::Response, +) -> superposition::Result<(StatusCode, superposition::ErrorResponse)> { + let status_code = response.status(); + let error_response = response + .json::() + .await + .map_err(|err: reqwest::Error| { + log::error!("failed to parse error response: {}", err); + unexpected_error!("Something went wrong") + })?; + log::error!("http call to CAC failed with err {:?}", error_response); + + Ok((status_code, error_response)) +} + +async fn process_cac_http_response( + response: Result, +) -> superposition::Result> { + let internal_server_error = unexpected_error!("Something went wrong."); + match response { + Ok(res) if res.status().is_success() => { + res.json::>().await.map_err(|err| { + log::error!("failed to parse JSON response with error: {}", err); + internal_server_error + }) + } + Ok(res) => { + log::error!("http call to CAC failed with status_code {}", res.status()); + + if res.status().is_client_error() { + let (status_code, error_response) = parse_error_response(res).await?; + Err(response_error!(status_code, error_response.message)) + } else { + Err(internal_server_error) + } + } + Err(err) => { + log::error!("reqwest failed to send request to CAC with error: {}", err); + Err(internal_server_error) + } + } +} + +#[post("")] +async fn create( + state: Data, + req: web::Json, + db_conn: DbConnection, + tenant: Tenant, + user: User, +) -> superposition::Result> { + use crate::db::schema::experiments::dsl::experiments; + let mut variants = req.variants.to_vec(); + let DbConnection(mut conn) = db_conn; + + // Checking if experiment has exactly 1 control variant, and + // atleast 1 experimental variant + check_variant_types(&variants)?; + let unique_override_keys: Vec = extract_override_keys(&variants[0].overrides) + .into_iter() + .collect(); + + let unique_ids_of_variants_from_req: HashSet<&str> = + HashSet::from_iter(variants.iter().map(|v| v.id.as_str())); + + if unique_ids_of_variants_from_req.len() != variants.len() { + return Err(bad_argument!( + "Variant ids are expected to be unique. Provide unqiue variant IDs" + )); + } + validate_override_keys(&unique_override_keys)?; + + // Checking if all the variants are overriding the mentioned keys + let variant_overrides = variants + .iter() + .map(|variant| &variant.overrides) + .collect::>>(); + let are_valid_variants = + check_variants_override_coverage(&variant_overrides, &unique_override_keys); + if !are_valid_variants { + return Err(bad_argument!( + "all variants should contain the keys mentioned in override_keys. Check if any of the following keys [{}] are missing from keys in your variants", + unique_override_keys.join(",") + ) + ); + } + + // Checking if context is a key-value pair map + if !req.context.is_object() { + return Err(bad_argument!("Context should be map of key value pairs.")); + } + + // validating experiment against other active experiments based on permission flags + let flags = &state.experimentation_flags; + let (valid, reason) = validate_experiment( + &req.context, + &unique_override_keys, + None, + &flags, + &mut conn, + )?; + if !valid { + return Err(bad_argument!(reason)); + } + + // generating snowflake id for experiment + let mut snowflake_generator = state.snowflake_generator.lock().unwrap(); + let experiment_id = snowflake_generator.real_time_generate(); + + //create overrides in CAC, if successfull then create experiment in DB + let mut cac_operations: Vec = vec![]; + for variant in &mut variants { + let variant_id = experiment_id.to_string() + "-" + &variant.id; + + // updating variant.id to => experiment_id + variant.id + variant.id = variant_id.to_string(); + + let updated_cacccontext = + add_variant_dimension_to_ctx(&req.context, variant_id.to_string())?; + + let payload = ContextPutReq { + context: updated_cacccontext + .as_object() + .ok_or_else(|| { + log::error!("Could not convert updated CAC context to serde Object"); + unexpected_error!( + "Something went wrong, failed to create experiment contexts" + ) + })? + .clone(), + r#override: json!(variant.overrides), + }; + cac_operations.push(ContextAction::PUT(payload)); + } + + // creating variants' context in CAC + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + + // Step 1: Perform the HTTP request and handle errors + let response = http_client + .put(&url) + .header("x-tenant", tenant.as_str()) + .header( + "Authorization", + format!("{} {}", user.get_auth_type(), user.get_auth_token()), + ) + .json(&cac_operations) + .send() + .await; + + // directly return an error response if not a 200 response + let created_contexts = process_cac_http_response(response).await?.into_iter().fold( + Vec::new(), + |mut acc, item| { + if let ContextBulkResponse::PUT(context) = item { + acc.push(context); + } else { + log::error!("Unexpected response item: {:?}", item); + } + acc + }, + ); + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; + variants[i].context_id = Some(created_context.context_id.clone()); + variants[i].override_id = Some(created_context.override_id.clone()); + } + + // inserting experiment in db + let new_experiment = Experiment { + id: experiment_id, + created_by: user.get_email(), + created_at: Utc::now(), + last_modified: Utc::now(), + name: req.name.to_string(), + override_keys: unique_override_keys.to_vec(), + traffic_percentage: 0, + status: ExperimentStatusType::CREATED, + context: req.context.clone(), + variants: serde_json::to_value(variants).unwrap(), + last_modified_by: user.get_email(), + chosen_variant: None, + }; + + let mut inserted_experiments = diesel::insert_into(experiments) + .values(&new_experiment) + .get_results(&mut conn)?; + + let inserted_experiment: Experiment = inserted_experiments.remove(0); + let response = ExperimentCreateResponse::from(inserted_experiment); + + return Ok(Json(response)); +} + +#[patch("/{experiment_id}/conclude")] +async fn conclude_handler( + state: Data, + path: web::Path, + req: web::Json, + db_conn: DbConnection, + tenant: Tenant, + user: User, +) -> superposition::Result> { + let DbConnection(conn) = db_conn; + let response = conclude( + state, + path.into_inner(), + req.into_inner(), + conn, + tenant, + user, + ) + .await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +pub async fn conclude( + state: Data, + experiment_id: i64, + req: ConcludeExperimentRequest, + mut conn: PooledConnection>, + tenant: Tenant, + user: User, +) -> superposition::Result { + use crate::db::schema::experiments::dsl; + + let winner_variant_id: String = req.chosen_variant.to_owned(); + + let experiment: Experiment = dsl::experiments + .find(experiment_id) + .get_result::(&mut conn)?; + + if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { + return Err(bad_argument!( + "experiment with id {} is already concluded", + experiment_id + )); + } + + let experiment_context = experiment.context.as_object().ok_or_else(|| { + log::error!("could not convert the context read from DB to JSON object"); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; + + let mut operations: Vec = vec![]; + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|err| { + log::error!( + "failed parse eixisting experiment variant while concluding with error: {}", + err + ); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; + + let mut is_valid_winner_variant = false; + for variant in experiment_variants { + let context_id = variant.context_id.ok_or_else(|| { + log::error!("context id not available for variant {:?}", variant.id); + unexpected_error!("Something went wrong, failed to conclude experiment") + })?; + + if variant.id == winner_variant_id { + let context_move_req = ContextMoveReq { + context: experiment_context.clone(), + }; + + is_valid_winner_variant = true; + + operations.push(ContextAction::MOVE((context_id, context_move_req))); + } else { + // delete this context + operations.push(ContextAction::DELETE(context_id)); + } + } + + if !is_valid_winner_variant { + return Err(bad_argument!( + "winner variant not found. A wrong variant id may have been sent, check and try again" + )); + } + + // calling CAC bulk api with operations as payload + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + let response = http_client + .put(&url) + .header("x-tenant", tenant.as_str()) + .header( + "Authorization", + format!("{} {}", user.get_auth_type(), user.get_auth_token()), + ) + .json(&operations) + .send() + .await; + + let _ = process_cac_http_response(response).await?; + + // updating experiment status in db + let updated_experiment = diesel::update(dsl::experiments) + .filter(dsl::id.eq(experiment_id)) + .set(( + dsl::status.eq(ExperimentStatusType::CONCLUDED), + dsl::last_modified.eq(Utc::now()), + dsl::last_modified_by.eq(user.get_email()), + dsl::chosen_variant.eq(Some(winner_variant_id)), + )) + .get_result::(&mut conn)?; + + return Ok(updated_experiment); +} + +#[get("")] +async fn list_experiments( + req: HttpRequest, + filters: Query, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + + let max_event_timestamp: Option = event_log::event_log + .filter(event_log::table_name.eq("experiments")) + .select(diesel::dsl::max(event_log::timestamp)) + .first(&mut conn)?; + + let last_modified = req + .headers() + .get("If-Modified-Since") + .and_then(|header_val| header_val.to_str().ok()) + .and_then(|header_str| { + DateTime::parse_from_rfc2822(header_str) + .map(|datetime| datetime.with_timezone(&Utc).naive_utc()) + .ok() + }); + + if max_event_timestamp.is_some() && max_event_timestamp < last_modified { + return Ok(HttpResponse::NotModified().finish()); + }; + + let query_builder = |filters: &ListFilters| { + let mut builder = experiments::experiments.into_boxed(); + if let Some(states) = filters.status.clone() { + builder = builder.filter(experiments::status.eq_any(states.0.clone())); + } + let now = Utc::now(); + builder + .filter( + experiments::last_modified + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(experiments::last_modified.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(experiments::last_modified.desc()) + .limit(limit) + .offset(offset); + + let number_of_experiments = count_query.count().get_result(&mut conn)?; + + 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(|entry| ExperimentResponse::from(entry)) + .collect(), + })) +} + +#[get("/{id}")] +async fn get_experiment_handler( + params: web::Path, + db_conn: DbConnection, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let response = get_experiment(params.into_inner(), &mut conn)?; + return Ok(Json(ExperimentResponse::from(response))); +} + +pub fn get_experiment( + experiment_id: i64, + conn: &mut PooledConnection>, +) -> superposition::Result { + use crate::db::schema::experiments::dsl::*; + let result: Experiment = experiments + .find(experiment_id) + .get_result::(conn)?; + + return Ok(result); +} + +#[patch("/{id}/ramp")] +async fn ramp( + params: web::Path, + req: web::Json, + db_conn: DbConnection, + user: User, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let exp_id = params.into_inner(); + + let experiment: Experiment = experiments::experiments + .find(exp_id) + .get_result::(&mut conn)?; + + let old_traffic_percentage = experiment.traffic_percentage as u8; + let new_traffic_percentage = req.traffic_percentage as u8; + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|e| { + log::error!( + "failed to parse existing experiment variants while ramping {}", + e + ); + unexpected_error!("Something went wrong, failed to ramp traffic percentage") + })?; + let variants_count = experiment_variants.len() as u8; + let max = 100 / variants_count; + + if matches!(experiment.status, ExperimentStatusType::CONCLUDED) { + return Err(bad_argument!( + "experiment already concluded, cannot ramp a concluded experiment" + )); + } else if new_traffic_percentage > max { + return Err(bad_argument!( + "The traffic_percentage cannot exceed {}. Provide a traffic percentage less than {}", max, max + ))?; + } else if new_traffic_percentage != 0 + && new_traffic_percentage == old_traffic_percentage + { + return Err(bad_argument!("The traffic_percentage is same as provided"))?; + } + let updated_experiment: Experiment = diesel::update(experiments::experiments) + .filter(experiments::id.eq(exp_id)) + .set(( + experiments::traffic_percentage.eq(req.traffic_percentage as i32), + experiments::last_modified.eq(Utc::now()), + experiments::last_modified_by.eq(user.get_email()), + experiments::status.eq(ExperimentStatusType::INPROGRESS), + )) + .get_result(&mut conn)?; + + return Ok(Json(ExperimentResponse::from(updated_experiment))); +} + +#[put("/{id}/overrides")] +async fn update_overrides( + params: web::Path, + state: Data, + db_conn: DbConnection, + req: web::Json, + tenant: Tenant, + user: User, +) -> superposition::Result> { + let DbConnection(mut conn) = db_conn; + let experiment_id = params.into_inner(); + + let payload = req.into_inner(); + let variants = payload.variants; + + let first_variant = variants.get(0).ok_or(bad_argument!( + "Variant not found in request. Provide at least one entry in variant's list", + ))?; + let override_keys = extract_override_keys(&first_variant.overrides) + .into_iter() + .collect(); + + // fetch the current variants of the experiment + let experiment = experiments::experiments + .find(experiment_id) + .first::(&mut conn)?; + + if experiment.status != ExperimentStatusType::CREATED { + return Err(bad_argument!( + "Only experiments in CREATED state can be updated" + )); + } + + let experiment_variants: Vec = serde_json::from_value(experiment.variants) + .map_err(|err| { + log::error!("failed to parse exisiting variants with error {}", err); + unexpected_error!("Something went wrong, failed to update experiment") + })?; + + let id_to_existing_variant: HashMap = HashMap::from_iter( + experiment_variants + .iter() + .map(|variant| (variant.id.to_string(), variant)) + .collect::>(), + ); + + /****************** Validating override_keys and variant overrides *********************/ + + validate_override_keys(&override_keys)?; + + // checking if variants passed with correct existing variant ids + let variant_ids: HashSet = HashSet::from_iter( + variants + .iter() + .map(|variant| (*variant).id.to_string()) + .collect::>(), + ); + for existing_id in id_to_existing_variant.keys() { + if !variant_ids.contains(existing_id) { + Err(bad_argument!( + "Some variant ids do not match with exisiting experiment variants. Provide all existing variants of the experiment" + ))?; + } + } + // Checking if all the variants are overriding the mentioned keys + let mut new_variants: Vec = variants + .into_iter() + .map(|variant| { + let existing_variant: &Variant = + id_to_existing_variant.get(&variant.id).unwrap(); + Variant { + id: variant.id, + variant_type: existing_variant.variant_type.clone(), + overrides: variant.overrides, + override_id: None, + context_id: None, + } + }) + .collect(); + + let variant_overrides = new_variants + .iter() + .map(|variant| &variant.overrides) + .collect::>>(); + let are_valid_variants = + check_variants_override_coverage(&variant_overrides, &override_keys); + if !are_valid_variants { + return Err( + bad_argument!( + "All variants should contain the keys mentioned in override_keys. Check if any of the following keys [{}] are missing from keys in your variants", + override_keys.join(",") + ) + )?; + } + + // validating experiment against other active experiments based on permission flags + let flags = &state.experimentation_flags; + let (valid, reason) = validate_experiment( + &experiment.context, + &override_keys, + Some(experiment_id), + &flags, + &mut conn, + )?; + if !valid { + return Err(bad_argument!(reason)); + } + + /******************************* Updating contexts ************************************/ + let mut cac_operations: Vec = vec![]; + + // adding operations to remove exisiting variant contexts + for existing_variant in experiment_variants { + let context_id = existing_variant.context_id.ok_or_else(|| { + log::error!( + "context id not available for variant {:?}", + existing_variant.id + ); + unexpected_error!("Something went wrong, failed to update experiment") + })?; + cac_operations.push(ContextAction::DELETE(context_id.to_string())); + } + + // adding operations to create new updated variant contexts + for variant in &mut new_variants { + let updated_cacccontext = + add_variant_dimension_to_ctx(&experiment.context, variant.id.to_string()) + .map_err(|e| { + log::error!("failed to add `variantIds` dimension to context: {e}"); + unexpected_error!("Something went wrong, failed to update experiment") + })?; + + let payload = ContextPutReq { + context: updated_cacccontext + .as_object() + .ok_or_else(|| { + log::error!("failed to parse updated context with variant dimension"); + unexpected_error!("Something went wrong, failed to update experiment") + })? + .clone(), + r#override: json!(variant.overrides), + }; + cac_operations.push(ContextAction::PUT(payload)); + } + + let http_client = reqwest::Client::new(); + let url = state.cac_host.clone() + "/context/bulk-operations"; + + let response = http_client + .put(&url) + .header("x-tenant", tenant.as_str()) + .header( + "Authorization", + format!("{} {}", user.get_auth_type(), user.get_auth_token()), + ) + .json(&cac_operations) + .send() + .await; + + // directly return an error response if not a 200 response + let created_contexts = process_cac_http_response(response).await?.into_iter().fold( + Vec::new(), + |mut acc, item| { + if let ContextBulkResponse::PUT(context) = item { + acc.push(context); + } else { + log::error!("Unexpected response item: {:?}", item); + } + acc + }, + ); + for i in 0..created_contexts.len() { + let created_context = &created_contexts[i]; + + new_variants[i].context_id = Some(created_context.context_id.clone()); + new_variants[i].override_id = Some(created_context.override_id.clone()); + } + + /*************************** Updating experiment in DB **************************/ + let new_variants_json = serde_json::to_value(new_variants).map_err(|e| { + log::error!("failed to serialize new variants to json with error: {e}"); + bad_argument!("failed to update experiment, bad variant data") + })?; + let updated_experiment = diesel::update(experiments::experiments.find(experiment_id)) + .set(( + experiments::variants.eq(new_variants_json), + experiments::override_keys.eq(override_keys), + experiments::last_modified.eq(Utc::now()), + experiments::last_modified_by.eq(user.get_email()), + )) + .get_result::(&mut conn)?; + + return Ok(Json(ExperimentResponse::from(updated_experiment))); +} + +#[get("/audit")] +async fn get_audit_logs( + filters: Query, + db_conn: DbConnection, +) -> superposition::Result { + let DbConnection(mut conn) = db_conn; + + let query_builder = |filters: &AuditQueryFilters| { + let mut builder = event_log::event_log.into_boxed(); + if let Some(tables) = filters.table.clone() { + builder = builder.filter(event_log::table_name.eq_any(tables.0)); + } + if let Some(actions) = filters.action.clone() { + builder = builder.filter(event_log::action.eq_any(actions.0)); + } + if let Some(username) = filters.username.clone() { + builder = builder.filter(event_log::user_name.eq(username)); + } + let now = Utc::now().naive_utc(); + builder + .filter( + event_log::timestamp + .ge(filters.from_date.unwrap_or(now - Duration::hours(24))), + ) + .filter(event_log::timestamp.le(filters.to_date.unwrap_or(now))) + }; + let filters = filters.into_inner(); + let base_query = query_builder(&filters); + let count_query = query_builder(&filters); + + let limit = filters.count.unwrap_or(10); + let offset = (filters.page.unwrap_or(1) - 1) * limit; + let query = base_query + .order(event_log::timestamp.desc()) + .limit(limit) + .offset(offset); + + let log_count: i64 = count_query.count().get_result(&mut conn)?; + + let logs: Vec = query.load(&mut conn)?; + + 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 + }))) +} diff --git a/crates/experimentation_platform/src/api/experiments/helpers.rs b/crates/experimentation_platform/src/api/experiments/helpers.rs new file mode 100644 index 000000000..64a6cbe0d --- /dev/null +++ b/crates/experimentation_platform/src/api/experiments/helpers.rs @@ -0,0 +1,230 @@ +use super::types::{Variant, VariantType}; +use crate::db::models::{Experiment, ExperimentStatusType}; +use diesel::pg::PgConnection; +use diesel::{BoolExpressionMethods, ExpressionMethods, QueryDsl, RunQueryDsl}; +use serde_json::{Map, Value}; +use service_utils::helpers::extract_dimensions; +use service_utils::service::types::ExperimentationFlags; +use std::collections::HashSet; + +use service_utils::{bad_argument, result as superposition}; + +pub fn check_variant_types(variants: &Vec) -> superposition::Result<()> { + let mut experimental_variant_cnt = 0; + let mut control_variant_cnt = 0; + + for variant in variants { + match variant.variant_type { + VariantType::CONTROL => { + control_variant_cnt += 1; + } + VariantType::EXPERIMENTAL => { + experimental_variant_cnt += 1; + } + } + } + + if control_variant_cnt > 1 || control_variant_cnt == 0 { + return Err(bad_argument!( + "Experiment should have exactly 1 control variant. Ensure only one control variant is present" + )); + } else if experimental_variant_cnt < 1 { + return Err(bad_argument!( + "Experiment should have at least 1 experimental variant. Ensure only one control variant is present" + )); + } + + Ok(()) +} + +pub fn validate_override_keys(override_keys: &Vec) -> superposition::Result<()> { + let mut key_set: HashSet<&str> = HashSet::new(); + for key in override_keys { + if !key_set.insert(key) { + return Err(bad_argument!( + "override_keys are not unique. Remove duplicate entries in override_keys" + )); + } + } + + Ok(()) +} + +pub fn are_overlapping_contexts( + context_a: &Value, + context_b: &Value, +) -> superposition::Result { + let dimensions_a = extract_dimensions(context_a)?; + let dimensions_b = extract_dimensions(context_b)?; + + let dim_a_keys = dimensions_a.keys(); + let dim_b_keys = dimensions_b.keys(); + + let ref_keys = if dim_a_keys.len() > dim_b_keys.len() { + dim_b_keys + } else { + dim_a_keys + }; + + let mut is_overlapping = true; + for key in ref_keys { + let test = (dimensions_a.contains_key(key) && dimensions_b.contains_key(key)) + && (dimensions_a[key] == dimensions_b[key]); + is_overlapping = is_overlapping && test; + + if !test { + break; + } + } + + Ok(is_overlapping) +} + +pub fn check_variant_override_coverage( + variant_override: &Map, + override_keys: &Vec, +) -> bool { + if variant_override.keys().len() != override_keys.len() { + return false; + } + + for override_key in override_keys { + if variant_override.get(override_key).is_none() { + return false; + } + } + return true; +} + +pub fn check_variants_override_coverage( + variant_overrides: &Vec<&Map>, + override_keys: &Vec, +) -> bool { + for variant_override in variant_overrides { + if !check_variant_override_coverage(variant_override, override_keys) { + return false; + } + } + + return true; +} + +pub fn is_valid_experiment( + context: &Value, + override_keys: &Vec, + flags: &ExperimentationFlags, + active_experiments: &Vec, +) -> superposition::Result<(bool, String)> { + let mut valid_experiment = true; + let mut invalid_reason = String::new(); + if !flags.allow_same_keys_overlapping_ctx + || !flags.allow_diff_keys_overlapping_ctx + || !flags.allow_same_keys_non_overlapping_ctx + { + let override_keys_set: HashSet<_> = override_keys.iter().collect(); + for active_experiment in active_experiments.iter() { + let are_overlapping = + are_overlapping_contexts(context, &active_experiment.context) + .map_err(|e| { + log::info!("experiment validation failed with error: {e}"); + bad_argument!( + "Context overlap validation failed, given context overlaps with a running experiment's context. Overlapping contexts are not allowed currently as per your configuration" + ) + })?; + + let have_intersecting_key_set = active_experiment + .override_keys + .iter() + .any(|key| override_keys_set.contains(key)); + + let same_key_set = active_experiment + .override_keys + .iter() + .all(|key| override_keys_set.contains(key)); + + if !flags.allow_diff_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && !same_key_set); + } + if !flags.allow_same_keys_overlapping_ctx { + valid_experiment = + valid_experiment && !(are_overlapping && have_intersecting_key_set); + } + if !flags.allow_same_keys_non_overlapping_ctx { + valid_experiment = + valid_experiment && !(!are_overlapping && have_intersecting_key_set); + } + + if !valid_experiment { + invalid_reason.push_str("This current context overlaps with an existing experiment or the keys in the context are overlapping"); + break; + } + } + } + + Ok((valid_experiment, invalid_reason)) +} + +pub fn validate_experiment( + context: &Value, + override_keys: &Vec, + experiment_id: Option, + flags: &ExperimentationFlags, + conn: &mut PgConnection, +) -> superposition::Result<(bool, String)> { + use crate::db::schema::experiments::dsl as experiments_dsl; + + let active_experiments: Vec = experiments_dsl::experiments + .filter( + diesel::dsl::not(experiments_dsl::id.eq(experiment_id.unwrap_or_default())) + .and( + experiments_dsl::status + .eq(ExperimentStatusType::CREATED) + .or(experiments_dsl::status.eq(ExperimentStatusType::INPROGRESS)), + ), + ) + .load(conn)?; + + is_valid_experiment(context, override_keys, flags, &active_experiments) +} + +pub fn add_variant_dimension_to_ctx( + context_json: &Value, + variant: String, +) -> superposition::Result { + let context = context_json.as_object().ok_or(bad_argument!( + "Context not an object. Ensure the context provided obeys the rules of JSON logic" + ))?; + + let mut conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or(bad_argument!( + "Failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic" + ))? + .clone(), + None => vec![context_json.clone()], + }; + + let variant_condition = serde_json::json!({ + "in" : [ + variant, + { "var": "variantIds" } + ] + }); + conditions.push(variant_condition); + + let mut updated_ctx = Map::new(); + updated_ctx.insert(String::from("and"), serde_json::Value::Array(conditions)); + + match serde_json::to_value(updated_ctx) { + Ok(value) => Ok(value), + Err(_) => Err(bad_argument!( + "Failed to convert context to a valid JSON object. Check the request sent for correctness" + )), + } +} + +pub fn extract_override_keys(overrides: &Map) -> HashSet { + overrides.keys().map(String::from).collect() +} diff --git a/crates/experimentation_platform/src/api/experiments/mod.rs b/crates/experimentation_platform/src/api/experiments/mod.rs new file mode 100644 index 000000000..d61ff8763 --- /dev/null +++ b/crates/experimentation_platform/src/api/experiments/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod helpers; +pub mod types; +pub use handlers::endpoints; diff --git a/crates/experimentation_platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs new file mode 100644 index 000000000..d4a1c0b5e --- /dev/null +++ b/crates/experimentation_platform/src/api/experiments/types.rs @@ -0,0 +1,187 @@ +use chrono::{DateTime, NaiveDateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use service_utils::helpers::deserialize_stringified_list; + +use crate::db::models::{self, ExperimentStatusType}; + +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)] +pub enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option, + pub override_id: Option, + pub overrides: Map, +} + +/********** Experiment Create Req Types ************/ + +#[derive(Deserialize)] +pub struct ExperimentCreateRequest { + pub name: String, + + pub context: Value, + pub variants: Vec, +} + +#[derive(Serialize)] +pub struct ExperimentCreateResponse { + pub experiment_id: String, +} + +impl From for ExperimentCreateResponse { + fn from(experiment: models::Experiment) -> Self { + ExperimentCreateResponse { + experiment_id: experiment.id.to_string(), + } + } +} + +/********** Experiment Response Type **************/ +// Same as models::Experiments but `id` field is String +// JS have limitation of 53-bit integers, so on +// deserializing from JSON to JS Object will lead incorrect `id` values +#[derive(Serialize, Deserialize)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime, + pub created_by: String, + pub last_modified: DateTime, + + pub name: String, + pub override_keys: Vec, + pub status: models::ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, + pub chosen_variant: Option, +} + +impl From for ExperimentResponse { + fn from(experiment: models::Experiment) -> Self { + ExperimentResponse { + id: experiment.id.to_string(), + created_at: experiment.created_at, + created_by: experiment.created_by, + last_modified: experiment.last_modified, + + name: experiment.name, + override_keys: experiment.override_keys, + status: experiment.status, + traffic_percentage: experiment.traffic_percentage, + + context: experiment.context, + variants: experiment.variants, + chosen_variant: experiment.chosen_variant, + } + } +} + +#[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, +} + +/********** Context Bulk API Type *************/ + +#[derive(Deserialize, Serialize, Clone)] +pub struct ContextPutReq { + pub context: serde_json::Map, + pub r#override: Value, +} + +#[derive(Deserialize, Serialize)] +pub enum ContextAction { + PUT(ContextPutReq), + DELETE(String), + MOVE((String, ContextMoveReq)), +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ContextPutResp { + pub context_id: String, + pub override_id: String, + pub priority: i32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum ContextBulkResponse { + PUT(ContextPutResp), + DELETE(String), + MOVE(ContextPutResp), +} + +/********** List API Filter Type *************/ + +#[derive(Deserialize, Debug, Clone)] +pub struct StatusTypes( + #[serde(deserialize_with = "deserialize_stringified_list")] + pub Vec, +); + +#[derive(Deserialize, Debug)] +pub struct ListFilters { + pub status: Option, + pub from_date: Option>, + pub to_date: Option>, + pub page: Option, + pub count: Option, +} + +/********** Ramp API type **********/ +#[derive(Deserialize, Debug)] +pub struct RampRequest { + pub traffic_percentage: u64, +} + +/********** Update API type ********/ + +#[derive(Deserialize, Debug)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: Map, +} + +#[derive(Deserialize, Debug)] +pub struct OverrideKeysUpdateRequest { + pub variants: Vec, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct ContextMoveReq { + pub context: serde_json::Map, +} + +/*********** List Audit API Filter Type **************/ + +#[derive(Deserialize, Debug, Clone)] +pub struct StringArgs( + #[serde(deserialize_with = "deserialize_stringified_list")] pub Vec, +); + +#[derive(Debug, Clone, Deserialize)] +pub struct AuditQueryFilters { + pub from_date: Option, + pub to_date: Option, + pub table: Option, + pub action: Option, + pub username: Option, + pub count: Option, + pub page: Option, +} diff --git a/crates/experimentation_platform/src/api/mod.rs b/crates/experimentation_platform/src/api/mod.rs new file mode 100644 index 000000000..c5b8fbb5b --- /dev/null +++ b/crates/experimentation_platform/src/api/mod.rs @@ -0,0 +1 @@ +pub mod experiments; diff --git a/crates/experimentation_platform/src/db/mod.rs b/crates/experimentation_platform/src/db/mod.rs new file mode 100644 index 000000000..d5cbad7e2 --- /dev/null +++ b/crates/experimentation_platform/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod models; +pub mod schema; diff --git a/crates/experimentation_platform/src/db/models.rs b/crates/experimentation_platform/src/db/models.rs new file mode 100644 index 000000000..3711c011f --- /dev/null +++ b/crates/experimentation_platform/src/db/models.rs @@ -0,0 +1,54 @@ +use crate::db::schema::*; +use chrono::{DateTime, NaiveDateTime, Utc}; + +use diesel::{Insertable, Queryable, QueryableByName, Selectable}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, diesel_derive_enum::DbEnum, +)] +#[DbValueStyle = "UPPERCASE"] +#[ExistingTypePath = "crate::db::schema::sql_types::ExperimentStatusType"] +pub enum ExperimentStatusType { + CREATED, + CONCLUDED, + INPROGRESS, +} + +#[derive(QueryableByName, Queryable, Selectable, Insertable, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(primary_key(id))] +pub struct Experiment { + pub id: i64, + pub created_at: DateTime, + pub created_by: String, + pub last_modified: DateTime, + + pub name: String, + pub override_keys: Vec, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, + pub last_modified_by: String, + pub chosen_variant: Option, +} + +pub type Experiments = Vec; + +#[derive(Queryable, Selectable, Insertable, Serialize, Clone, Debug)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(table_name = event_log)] +#[diesel(primary_key(id))] +pub struct EventLog { + pub id: uuid::Uuid, + pub table_name: String, + pub user_name: String, + pub timestamp: NaiveDateTime, + pub action: String, + pub original_data: Option, + pub new_data: Option, + pub query: String, +} diff --git a/crates/experimentation_platform/src/db/schema.rs b/crates/experimentation_platform/src/db/schema.rs new file mode 100644 index 000000000..a8676f480 --- /dev/null +++ b/crates/experimentation_platform/src/db/schema.rs @@ -0,0 +1,619 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "experiment_status_type"))] + pub struct ExperimentStatusType; +} + +diesel::table! { + event_log (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2023m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2024m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2025m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m01 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m02 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m03 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m04 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m05 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m06 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m07 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m08 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m09 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m10 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m11 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + event_log_y2026m12 (id, timestamp) { + id -> Uuid, + table_name -> Text, + user_name -> Text, + timestamp -> Timestamp, + action -> Text, + original_data -> Nullable, + new_data -> Nullable, + query -> Text, + } +} + +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::ExperimentStatusType; + + experiments (id) { + id -> Int8, + created_at -> Timestamptz, + created_by -> Text, + last_modified -> Timestamptz, + name -> Text, + override_keys -> Array, + status -> ExperimentStatusType, + traffic_percentage -> Int4, + context -> Json, + variants -> Json, + last_modified_by -> Text, + chosen_variant -> Nullable, + } +} + +diesel::allow_tables_to_appear_in_same_query!( + event_log, + event_log_y2023m08, + event_log_y2023m09, + event_log_y2023m10, + event_log_y2023m11, + event_log_y2023m12, + event_log_y2024m01, + event_log_y2024m02, + event_log_y2024m03, + event_log_y2024m04, + event_log_y2024m05, + event_log_y2024m06, + event_log_y2024m07, + event_log_y2024m08, + event_log_y2024m09, + event_log_y2024m10, + event_log_y2024m11, + event_log_y2024m12, + event_log_y2025m01, + event_log_y2025m02, + event_log_y2025m03, + event_log_y2025m04, + event_log_y2025m05, + event_log_y2025m06, + event_log_y2025m07, + event_log_y2025m08, + event_log_y2025m09, + event_log_y2025m10, + event_log_y2025m11, + event_log_y2025m12, + event_log_y2026m01, + event_log_y2026m02, + event_log_y2026m03, + event_log_y2026m04, + event_log_y2026m05, + event_log_y2026m06, + event_log_y2026m07, + event_log_y2026m08, + event_log_y2026m09, + event_log_y2026m10, + event_log_y2026m11, + event_log_y2026m12, + experiments, +); diff --git a/crates/experimentation_platform/src/lib.rs b/crates/experimentation_platform/src/lib.rs new file mode 100644 index 000000000..037782038 --- /dev/null +++ b/crates/experimentation_platform/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod db; diff --git a/crates/experimentation_platform/src/schema.patch b/crates/experimentation_platform/src/schema.patch new file mode 100644 index 000000000..b9e8334f3 --- /dev/null +++ b/crates/experimentation_platform/src/schema.patch @@ -0,0 +1,25 @@ +diff --git a/crates/experimentation-platform/src/db/schema.rs b/crates/experimentation-platform/src/db/schema.rs +index 692c718..46a426c 100644 +--- a/crates/experimentation-platform/src/db/schema.rs ++++ b/crates/experimentation-platform/src/db/schema.rs +@@ -6,6 +6,2 @@ pub mod sql_types { + pub struct ExperimentStatusType; +- +- #[derive(diesel::sql_types::SqlType)] +- #[diesel(postgres_type(name = "not_null_text"))] +- pub struct NotNullText; + } +@@ -183,3 +179,2 @@ diesel::table! { + use diesel::sql_types::*; +- use super::sql_types::NotNullText; + use super::sql_types::ExperimentStatusType; +@@ -192,3 +187,3 @@ diesel::table! { + name -> Text, +- override_keys -> Array>, ++ override_keys -> Array, + status -> ExperimentStatusType, +@@ -217,2 +212,2 @@ diesel::allow_tables_to_appear_in_same_query!( + experiments, +-); ++); +\ No newline at end of file diff --git a/crates/experimentation_platform/tests/experimentation_tests.rs b/crates/experimentation_platform/tests/experimentation_tests.rs new file mode 100644 index 000000000..af7f8d1ed --- /dev/null +++ b/crates/experimentation_platform/tests/experimentation_tests.rs @@ -0,0 +1,576 @@ +use chrono::Utc; +use experimentation_platform::api::experiments::helpers; +use experimentation_platform::db::models::{Experiment, ExperimentStatusType}; +use serde_json::{json, Map, Value}; +use service_utils::helpers::extract_dimensions; +use service_utils::result::AppError; +use service_utils::service::types::ExperimentationFlags; + +enum Dimensions { + OS(String), + CLIENT(String), +} + +fn single_dimension_ctx_gen(value: Dimensions) -> serde_json::Value { + match value { + Dimensions::OS(os) => serde_json::json!({ + "==": [ + {"var": "os"}, + os + ] + }), + Dimensions::CLIENT(client_id) => serde_json::json!({ + "==": [ + client_id, + {"var": "clientId"} + ] + }), + } +} + +fn multiple_dimension_ctx_gen(values: Vec) -> serde_json::Value { + let mut conditions: Vec = vec![]; + for val in values { + conditions.push(single_dimension_ctx_gen(val)); + } + + serde_json::json!({ "and": conditions }) +} + +fn experiment_gen( + override_keys: &Vec, + context: &Value, + status: ExperimentStatusType, + variants: &Value, +) -> Experiment { + Experiment { + id: 123456789, + created_at: Utc::now(), + created_by: "test".to_string(), + last_modified: Utc::now(), + last_modified_by: "test".to_string(), + name: "experiment-test".to_string(), + traffic_percentage: 0, + + override_keys: override_keys.to_vec(), + status: status, + context: context.clone(), + variants: variants.clone(), + chosen_variant: None, + } +} + +#[test] +fn test_duplicate_override_key_entries() { + let override_keys = vec!["key1".to_string(), "key2".to_string(), "key1".to_string()]; + assert!(matches!( + helpers::validate_override_keys(&override_keys), + Err(AppError::BadArgument(_)) + )); +} + +#[test] +fn test_unique_override_key_entries() { + let override_keys = vec!["key1".to_string(), "key2".to_string()]; + assert!(matches!( + helpers::validate_override_keys(&override_keys), + Ok(()) + )); +} + +#[test] +fn test_extract_dimensions() -> Result<(), AppError> { + let context_a = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let context_b = + single_dimension_ctx_gen(Dimensions::CLIENT("testclient1".to_string())); + + let expected_dimensions_1 = serde_json::Map::from_iter(vec![ + ("os".to_string(), json!("os1")), + ("clientId".to_string(), json!("testclient1")), + ]); + let expected_dimensions_2 = + serde_json::Map::from_iter(vec![("clientId".to_string(), json!("testclient1"))]); + + // more than one dimension in context + assert_eq!(extract_dimensions(&context_a)?, expected_dimensions_1); + // only one dimension in context + assert_eq!(extract_dimensions(&context_b)?, expected_dimensions_2); + Ok(()) +} + +#[test] +fn test_are_overlapping_contexts() -> Result<(), AppError> { + let context_a = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let context_b = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]); + let context_c = single_dimension_ctx_gen(Dimensions::OS("os1".to_string())); + let context_d = single_dimension_ctx_gen(Dimensions::OS("os2".to_string())); + + // both contexts with same dimensions + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_a)?, + true + ); + // contexts with one different dimension + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_b)?, + false + ); + // one context dimensions are subset of other + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_c)?, + true + ); + // one context dimensions not a subset of other but have less dimensions that other + assert_eq!( + helpers::are_overlapping_contexts(&context_a, &context_d)?, + false + ); + // disjoint contexts + assert_eq!( + helpers::are_overlapping_contexts(&context_c, &context_d)?, + false + ); + Ok(()) +} + +#[test] +fn test_check_variants_override_coverage() { + let override_keys = vec!["key1".to_string(), "key2".to_string()]; + let overrides = vec![ + // has all mentioned override keys + Map::from_iter(vec![ + ("key1".to_string(), json!("value1")), + ("key2".to_string(), json!("value2")), + ]), + // has one override key mi)ssing + Map::from_iter(vec![("key1".to_string(), json!("value1"))]), + // has an unknown override) key + Map::from_iter(vec![("key3".to_string(), json!("value3"))]), + // has an extra unknown ov)erride key + Map::from_iter(vec![ + ("key1".to_string(), json!("value1")), + ("key2".to_string(), json!("value2")), + ("key3".to_string(), json!("value3")), + ]), + ]; + + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[0], &override_keys), + true + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[1], &override_keys), + false + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[2], &override_keys), + false + ); + assert_eq!( + helpers::check_variant_override_coverage(&r#overrides[3], &override_keys), + false + ); +} + +/************************* No Restrictions *****************************************/ + +#[test] +fn test_is_valid_experiment_no_restrictions_overlapping_experiment( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key2".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_no_restrictions_non_overlapping_experiment( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key2".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Same Keys Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_one_same_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_overlapping_ctx_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: false, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Different Keys Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_one_diff_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_diff_keys_overlapping_ctx_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &experiment_context, + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +/************************* Restrict Same Keys Non Overlapping Context *****************************************/ + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_same_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: false, + }; + + let active_experiments = vec![experiment_gen( + &experiment_override_keys, + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_one_diff_key( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: true, + allow_same_keys_non_overlapping_ctx: false, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key1".to_string(), "key3".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (false, "This current context overlaps with an existing experiment or the keys in the context are overlapping".to_string()) + ); + + Ok(()) +} + +#[test] +fn test_is_valid_experiment_restrict_same_keys_non_overlapping_ctx_non_overlapping_experiment_diff_keys( +) -> Result<(), AppError> { + let experiment_context = multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os1".to_string()), + Dimensions::CLIENT("testclient1".to_string()), + ]); + let experiment_override_keys = vec!["key1".to_string(), "key2".to_string()]; + let flags = ExperimentationFlags { + allow_same_keys_overlapping_ctx: true, + allow_diff_keys_overlapping_ctx: false, + allow_same_keys_non_overlapping_ctx: true, + }; + + let active_experiments = vec![experiment_gen( + &vec!["key3".to_string(), "key4".to_string()], + &multiple_dimension_ctx_gen(vec![ + Dimensions::OS("os2".to_string()), + Dimensions::CLIENT("testclient2".to_string()), + ]), + ExperimentStatusType::CREATED, + &json!(""), + )]; + + assert_eq!( + helpers::is_valid_experiment( + &experiment_context, + &experiment_override_keys, + &flags, + &active_experiments + )?, + (true, "".to_string()) + ); + + Ok(()) +} diff --git a/crates/external/CHANGELOG.md b/crates/external/CHANGELOG.md new file mode 100644 index 000000000..d1873c3e6 --- /dev/null +++ b/crates/external/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## external-v0.4.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + +## external-v0.3.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## external-v0.2.0 - 2023-11-08 +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + +## external-v0.1.0 - 2023-11-06 +#### Bug Fixes +- PICAF-25068 x-tenant header added for /config/resolve call in diff - (0e34c31) - Ritick Madaan +#### Features +- [PICAF-24563] added dashboard auth middleware - (955d9e9) - Kartik Gajendra + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/external/Cargo.toml b/crates/external/Cargo.toml new file mode 100644 index 000000000..60f971bcb --- /dev/null +++ b/crates/external/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "external" +version = "0.4.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +log = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +reqwest = { workspace = true } +anyhow = { workspace = true } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", tag = "v1.9.0" } diff --git a/crates/external/src/api/external_api/handlers.rs b/crates/external/src/api/external_api/handlers.rs new file mode 100644 index 000000000..44ad8089d --- /dev/null +++ b/crates/external/src/api/external_api/handlers.rs @@ -0,0 +1,119 @@ +use actix_web::{ + get, patch, + web::{self, Data, Json}, + Scope, +}; + +use crate::api::external_api::{ + helpers::{fetch_variant_id, get_resolved_config}, + types::DiffResponse, +}; +use experimentation_platform::{ + api::experiments::{ + handlers::{conclude, get_experiment}, + types::{ConcludeExperimentRequest, ExperimentResponse, VariantType}, + }, + db::models::Experiment, +}; +use serde_json::Value; +use service_utils::{ + helpers::extract_dimensions, + result, + service::types::{AppState, DbConnection, Tenant}, +}; + +use superposition_types::User; + +pub fn endpoints(scope: Scope) -> Scope { + scope + .service(stabilize) + .service(revert) + .service(diff_handler) +} + +#[patch("/{id}/stabilize")] +async fn stabilize( + params: web::Path, + state: Data, + db_conn: DbConnection, + tenant: Tenant, + user: User, +) -> result::Result> { + let response = conclude_experiment( + params.into_inner(), + state, + db_conn, + tenant, + VariantType::EXPERIMENTAL, + user, + ) + .await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +#[patch("/{id}/revert")] +async fn revert( + params: web::Path, + state: Data, + db_conn: DbConnection, + tenant: Tenant, + user: User, +) -> result::Result> { + let response = conclude_experiment( + params.into_inner(), + state, + db_conn, + tenant, + VariantType::CONTROL, + user, + ) + .await?; + return Ok(Json(ExperimentResponse::from(response))); +} + +pub async fn conclude_experiment( + exp_id: i64, + state: Data, + db_conn: DbConnection, + tenant: Tenant, + variant: VariantType, + user: User, +) -> result::Result { + let DbConnection(mut conn) = db_conn; + + let experiment = get_experiment(exp_id, &mut conn)?; + let id = fetch_variant_id(&experiment, variant)?; + let req_body = ConcludeExperimentRequest { + chosen_variant: id.to_string(), + }; + let response = conclude(state, exp_id, req_body, conn, tenant, user).await?; + return Ok(response); +} + +#[get("/{id}/diff")] +pub async fn diff_handler( + params: web::Path, + state: Data, + db_conn: DbConnection, +) -> result::Result> { + let DbConnection(mut conn) = db_conn; + let exp_id = params.into_inner(); + let experiment = get_experiment(exp_id, &mut conn)?; + let mut req = extract_dimensions(&experiment.context)?; + let control_id = fetch_variant_id(&experiment, VariantType::CONTROL)?; + let experimental_id = fetch_variant_id(&experiment, VariantType::EXPERIMENTAL)?; + + req.insert( + "variantIds".to_string(), + Value::String(format!("[{}]", control_id)), + ); + let before = get_resolved_config(&state, &req).await?; + req.insert( + "variantIds".to_string(), + Value::String(format!("[{}]", experimental_id)), + ); + let after = get_resolved_config(&state, &req).await?; + + let res = DiffResponse { before, after }; + return Ok(Json(res)); +} diff --git a/crates/external/src/api/external_api/helpers.rs b/crates/external/src/api/external_api/helpers.rs new file mode 100644 index 000000000..9881abdc8 --- /dev/null +++ b/crates/external/src/api/external_api/helpers.rs @@ -0,0 +1,51 @@ +use actix_web::web::Data; +use experimentation_platform::{ + api::experiments::types::{Variant, VariantType}, + db::models::Experiment, +}; + +use serde_json::{Map, Value}; +use service_utils::{result, service::types::AppState, unexpected_error}; + +pub fn fetch_variant_id( + experiment: &Experiment, + variant: VariantType, +) -> result::Result { + let variants = &experiment.variants; + let experiment_variants: Vec = serde_json::from_value(variants.clone()) + .map_err(|e| { + log::error!("parsing to variant type failed with err: {e}"); + unexpected_error!("Something went wrong.") + })?; + + for ele in experiment_variants { + if ele.variant_type == variant { + return Ok(ele.id); + } + } + log::info!( + "Failed to fetch variant {:?} id for exp {}", + variant, + experiment.id + ); + return Err(unexpected_error!("Something went wrong.")); +} + +pub async fn get_resolved_config( + state: &Data, + dimension_ctx: &Map, +) -> result::Result { + let http_client = reqwest::Client::new(); + let url = format!("{}/config/resolve", state.cac_host); + let resp = http_client + .get(&url) + .header("x-tenant", "mjos") + .query(dimension_ctx) + .send() + .await + .map_err(|e| unexpected_error!(e))? + .json() + .await + .map_err(|e| unexpected_error!(e))?; + Ok(resp) +} diff --git a/crates/external/src/api/external_api/mod.rs b/crates/external/src/api/external_api/mod.rs new file mode 100644 index 000000000..d61ff8763 --- /dev/null +++ b/crates/external/src/api/external_api/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod helpers; +pub mod types; +pub use handlers::endpoints; diff --git a/crates/external/src/api/external_api/types.rs b/crates/external/src/api/external_api/types.rs new file mode 100644 index 000000000..ef3c588d2 --- /dev/null +++ b/crates/external/src/api/external_api/types.rs @@ -0,0 +1,8 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize)] +pub struct DiffResponse { + pub before: Value, + pub after: Value, +} diff --git a/crates/external/src/api/mod.rs b/crates/external/src/api/mod.rs new file mode 100644 index 000000000..756165ffc --- /dev/null +++ b/crates/external/src/api/mod.rs @@ -0,0 +1 @@ +pub mod external_api; diff --git a/crates/external/src/lib.rs b/crates/external/src/lib.rs new file mode 100644 index 000000000..00472df1b --- /dev/null +++ b/crates/external/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub use api::external_api::endpoints; diff --git a/crates/frontend/.gitignore b/crates/frontend/.gitignore new file mode 100644 index 000000000..8cdaa33de --- /dev/null +++ b/crates/frontend/.gitignore @@ -0,0 +1,13 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +pkg + +# These are backup files generated by rustfmt +**/*.rs.bk + +# node e2e test tools and outputs +node_modules/ +test-results/ +end2end/playwright-report/ +playwright/.cache/ diff --git a/crates/frontend/CHANGELOG.md b/crates/frontend/CHANGELOG.md new file mode 100644 index 000000000..13dcf4adc --- /dev/null +++ b/crates/frontend/CHANGELOG.md @@ -0,0 +1,110 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## frontend-v0.5.1 - 2024-04-12 +#### Bug Fixes +- [PICAF-26529] function route fix - (aba54da) - Pratik Mishra + +- - - + +## frontend-v0.5.0 - 2024-04-10 +#### Features +- PICAF-26529 Add function support in dimension and default config form - (6a942fe) - ankit.mahato +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + +## frontend-v0.4.0 - 2024-04-05 +#### Features +- [PICAF-26360] added decimal support in context and override form and fixed dimension modal - (3f1f998) - Saurav Suman + +- - - + +## frontend-v0.3.0 - 2024-04-05 +#### Features +- [PICAF-26522] added support for dynamic json schema in frontend - (b02f08f) - Saurav Suman +- [PICAF-26346] function ui - (9360aca) - Pratik Mishra + +- - - + +## frontend-v0.2.1 - 2024-03-21 +#### Bug Fixes +- PICAF-26324 added type for condition - (3607336) - Saurav CV +- PICAF-26324 ui bug fix for contexts - (7ec15ec) - Saurav CV + +- - - + +## frontend-v0.2.0 - 2024-03-21 +#### Features +- [PICAF-26197] refactor resolve page - (acc763a) - Kartik + +- - - + +## frontend-v0.1.1 - 2024-03-07 +#### Bug Fixes +- adding min-width settings for table component - (0fcd0c1) - Kartik + +- - - + +## frontend-v0.1.0 - 2024-03-06 +#### Bug Fixes +- added drawer, improved UX & single click override addition to variants - (14a1ead) - Shubhranshu Sanjeev +- [PICAF-26199] transpose columns in single experiment page for variants - (a1a8ac8) - Kartik +- [PICAF-26196] add traffic percentage to experiments table - (5fb0221) - Kartik +- [PICAF-26195] fix copy of experiment ID - (37e4c24) - Kartik +- using SERVICE_NAME in is_server instead of SERVER_NAME(wrong var name) - (efe97f0) - Shubhranshu Sanjeev +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins +- refactored experiment page and fixed experiment edit flow - (b153486) - Shubhranshu Sanjeev +- getting api hostname from env for frontend - (837899d) - Shubhranshu Sanjeev +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev +- refactored DefaultConfig component + fixed edit flow - (f2d38cc) - Shubhranshu Sanjeev +- fixed dimension form edit flow + fixed table component CellFormatter to accept move closures - (9c3a364) - Shubhranshu Sanjeev +- frontend build process - (cbdad01) - Shubhranshu Sanjeev +- fixed tenant hydration bug - (cf0e633) - Saurav Suman +- fixed ci-test to support multi-tenant setup - (916b75d) - Shubhranshu Sanjeev +- cleanup code - (4820f31) - Kartik Gajendra +- UI fixes for demo - (4927766) - Kartik Gajendra +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +- fixed experiment list page feedback - (f406264) - Shubhranshu Sanjeev +- context parsing - (d46ca42) - Kartik Gajendra +- resolve UI bugs - (98695a8) - Kartik Gajendra +- dimensions page updates - (5220b36) - ankit.mahato +#### Features +- PICAF-26266 url click and text wrap fixes - (643c54d) - Saurav CV +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev +- added bool, i64 and decimal in default config form - (fca1ca6) - Saurav Suman +- [PICAF-25817] added authentication header for frontend apis - (3f90592) - Saurav Suman +- added between in frontend - (0eb60e5) - Akhilesh Bhadauriya +- added validation inside default config form , formatted dates , added disable feature of edit - (cacf20f) - Saurav Suman +- resolve page with unified UI - (e84eb41) - Kartik Gajendra +- working resolve page - (803dfbd) - Kartik Gajendra +- fixed experiment suspense block , added generic button - (117bfc8) - Saurav Suman +- experiment create form - (91371c0) - Shubhranshu Sanjeev +- fixed theme + ui changes + form validation + context validation error handling - (6cf5929) - Saurav Suman +- working resolve page - (81c83d4) - Kartik Gajendra +- added state changes in the form - (b64a227) - Saurav Suman +- testing create form - (d0a5aea) - Kartik Gajendra +- working experiments page - (81b17dc) - Kartik Gajendra +- experiment UI - (72e19e6) - Kartik Gajendra +- added default config and override screen - (cd4267e) - Saurav Suman +- added default config page - (95e909d) - Saurav Suman +- working experiments page - (9a1d74c) - Kartik Gajendra +- override and context form - (553e3ad) - Shubhranshu Sanjeev +- dimensions - (c5e94fa) - ankit.mahato +- added experiment-list page - (ee462fd) - Shubhranshu Sanjeev +- experiment UI - (24e1b56) - Kartik Gajendra +- ui for cac and exp - (41f884f) - Shubhranshu Sanjeev +- added frontend crate,combined frontend and backend binaries (PICAF-24540) - (ee084ba) - Saurav Suman +#### Miscellaneous Chores +- formatted code + cleanup - (6d4874b) - Shubhranshu Sanjeev +- formatted frontend code - (70f873f) - Shubhranshu Sanjeev +#### Refactoring +- using snake case for component fxn names - (19e9aca) - Shubhranshu Sanjeev +- fixed warnings, added redirection for home page and script for setting up the project - (6b21fb9) - Saurav Suman + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml new file mode 100644 index 000000000..36ed1e2a7 --- /dev/null +++ b/crates/frontend/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "frontend" +version = "0.5.1" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +dotenv = { workspace = true } +actix-files = { version = "0.6", optional = true } +actix-web = { version = "4.5.0", optional = true, features = ["macros"] } +console_error_panic_hook = "0.1" +cfg-if = "1" +http = { version = "0.2", optional = true } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_actix = { version = "0.5.2", optional = true } +leptos_router = { workspace = true } +wasm-bindgen = "=0.2.89" +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +web-sys = "0.3.64" +futures = "0.3" +derive_more = { workspace = true } +anyhow = { workspace = true } +chrono = {workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } +js-sys = "0.3.65" +url = "2.5.0" + + +[features] +csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] +ssr = [ + "dep:actix-files", + "dep:actix-web", + "dep:leptos_actix", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] diff --git a/crates/frontend/assets/favicon.ico b/crates/frontend/assets/favicon.ico new file mode 100644 index 000000000..2ba8527cb Binary files /dev/null and b/crates/frontend/assets/favicon.ico differ diff --git a/crates/frontend/end2end/package-lock.json b/crates/frontend/end2end/package-lock.json new file mode 100644 index 000000000..f12af4425 --- /dev/null +++ b/crates/frontend/end2end/package-lock.json @@ -0,0 +1,74 @@ +{ + "name": "end2end", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "end2end", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.28.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + } + }, + "dependencies": { + "@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.0" + } + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true + } + } +} diff --git a/crates/frontend/end2end/package.json b/crates/frontend/end2end/package.json new file mode 100644 index 000000000..ed785859f --- /dev/null +++ b/crates/frontend/end2end/package.json @@ -0,0 +1,13 @@ +{ + "name": "end2end", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0" + } +} diff --git a/crates/frontend/end2end/playwright.config.ts b/crates/frontend/end2end/playwright.config.ts new file mode 100644 index 000000000..e9891c094 --- /dev/null +++ b/crates/frontend/end2end/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: "./tests", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/crates/frontend/end2end/tests/example.spec.ts b/crates/frontend/end2end/tests/example.spec.ts new file mode 100644 index 000000000..a461f351a --- /dev/null +++ b/crates/frontend/end2end/tests/example.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from "@playwright/test"; + +test("homepage has title and links to intro page", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + await expect(page).toHaveTitle("Welcome to Leptos"); + + await expect(page.locator("h1")).toHaveText("Welcome to Leptos!"); +}); diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs new file mode 100644 index 000000000..a8f3db2be --- /dev/null +++ b/crates/frontend/src/api.rs @@ -0,0 +1,169 @@ +use leptos::ServerFnError; + +use crate::{ + types::{ + Config, DefaultConfig, Dimension, Experiment, ExperimentsResponse, + FunctionResponse, ListFilters, + }, + utils::use_host_server, +}; + +// #[server(GetDimensions, "/fxn", "GetJson")] +pub async fn fetch_dimensions(tenant: String) -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/dimension", host); + let response: Vec = client + .get(url) + .header("x-tenant", &tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +// #[server(GetDefaultConfig, "/fxn", "GetJson")] +pub async fn fetch_default_config( + tenant: String, +) -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/default-config", host); + let response: Vec = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +// #[server(GetExperiments, "/fxn", "GetJson")] +pub async fn fetch_experiments( + filters: ListFilters, + tenant: String, +) -> Result { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let mut query_params = vec![]; + if let Some(status) = filters.status { + let status: Vec = status.iter().map(|val| val.to_string()).collect(); + query_params.push(format!("status={}", status.join(","))); + } + if let Some(from_date) = filters.from_date { + query_params.push(format!("from_date={}", from_date)); + } + if let Some(to_date) = filters.to_date { + query_params.push(format!("to_date={}", to_date)); + } + if let Some(page) = filters.page { + query_params.push(format!("page={}", page)); + } + if let Some(count) = filters.count { + query_params.push(format!("count={}", count)); + } + + let url = format!("{}/experiments?{}", host, query_params.join("&")); + let response: ExperimentsResponse = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +pub async fn fetch_functions( + tenant: String, +) -> Result, ServerFnError> { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/function", host); + let response: Vec = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +pub async fn fetch_function( + function_name: String, + tenant: String, +) -> Result { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/function/{}", host, function_name); + let response: FunctionResponse = client + .get(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))? + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + + Ok(response) +} + +// #[server(GetConfig, "/fxn", "GetJson")] +pub async fn fetch_config(tenant: String) -> Result { + let client = reqwest::Client::new(); + let host = use_host_server(); + + let url = format!("{}/config", host); + match client.get(url).header("x-tenant", tenant).send().await { + Ok(response) => { + let config: Config = response + .json() + .await + .map_err(|e| ServerFnError::ServerError(e.to_string()))?; + Ok(config) + } + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + } +} + +// #[server(GetExperiment, "/fxn", "GetJson")] +pub async fn fetch_experiment( + exp_id: String, + tenant: 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 { + Ok(experiment) => { + let experiment = experiment + .json() + .await + .map_err(|err| ServerFnError::ServerError(err.to_string()))?; + Ok(experiment) + } + Err(e) => Err(ServerFnError::ServerError(e.to_string())), + } +} diff --git a/crates/frontend/src/app.rs b/crates/frontend/src/app.rs new file mode 100644 index 000000000..398391e3f --- /dev/null +++ b/crates/frontend/src/app.rs @@ -0,0 +1,216 @@ +use leptos::*; +use leptos_meta::*; +use leptos_router::*; +use serde_json::json; + +use crate::hoc::layout::layout::Layout; +use crate::pages::experiment_list::experiment_list::ExperimentList; +use crate::pages::function::{ + function::FunctionPage, function_create::CreateFunctionView, + function_list::FunctionList, +}; +use crate::pages::Dimensions::Dimensions::Dimensions; +use crate::pages::{ + ContextOverride::context_override::ContextOverride, + DefaultConfig::DefaultConfig::DefaultConfig, Experiment::ExperimentPage, + Home::Home::Home, +}; +use crate::types::Envs; + +#[component] +pub fn App(app_envs: Envs) -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + let service_prefix = app_envs.service_prefix; + provide_context(app_envs.clone()); + view! { + + {move || { + let base = match service_prefix { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + let styles_href = base.to_owned() + "/pkg/style.css"; + let favicon_href = base.to_owned() + "/assets/favicon.ico"; + let wasm_href = base.to_owned() + "/pkg/frontend_bg.wasm"; + let js_href = base.to_owned() + "/pkg/frontend.js"; + let import_callback = "() => mod.hydrate()"; + view! { + + + + {move || { + if base == "" { + view! {}.into_view() + } else { + view! { + + + + } + .into_view() + } + }} + } + }} + // sets the document title + + <script type_="text/javascript">"__APP_ENVS=" {json!(app_envs).to_string()}</script> + <Router base=service_prefix> + <body class="m-0 min-h-screen bg-gray-50 font-mono"> + <Routes base=service_prefix.to_string()> + <Route + ssr=SsrMode::Async + path="/admin/:tenant/dimensions" + view=move || { + view! { + <Layout> + <Dimensions/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function" + view=move || { + view! { + <Layout> + <FunctionList/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function/create" + view=move || { + view! { + <Layout> + <CreateFunctionView/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/function/:function_name" + view=move || { + view! { + <Layout> + <FunctionPage/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/experiments" + view=move || { + view! { + <Layout> + <ExperimentList/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/experiments/:id" + view=move || { + view! { + <Layout> + <ExperimentPage/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/default-config" + view=move || { + view! { + <Layout> + <DefaultConfig/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/overrides" + view=move || { + view! { + <Layout> + <ContextOverride/> + </Layout> + } + } + /> + + <Route + ssr=SsrMode::Async + path="/admin/:tenant/resolve" + view=move || { + view! { + <Layout> + <Home/> + </Layout> + } + } + /> + + // <Route + // path="/*any" + // view=move || { + // view! { + // <Layout> + // <NotFound/> + // </Layout> + // } + // } + // /> + + </Routes> + </body> + </Router> + + </html> + } +} diff --git a/crates/frontend/src/components/button/button.rs b/crates/frontend/src/components/button/button.rs new file mode 100644 index 000000000..ad4d5bf39 --- /dev/null +++ b/crates/frontend/src/components/button/button.rs @@ -0,0 +1,24 @@ +use leptos::*; +use web_sys::MouseEvent; + +#[component] +pub fn button<F: Fn(MouseEvent) + 'static>( + text: String, + on_click: F, + #[prop(default = String::new())] class: String, + #[prop(default = String::new())] id: String, +) -> impl IntoView { + view! { + <button + class=format!( + "btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 {class}", + ) + + id=id + on:click=on_click + > + {text} + <i class="ri-edit-2-line ml-2"></i> + </button> + } +} diff --git a/crates/frontend/src/components/button/mod.rs b/crates/frontend/src/components/button/mod.rs new file mode 100644 index 000000000..aa200ca23 --- /dev/null +++ b/crates/frontend/src/components/button/mod.rs @@ -0,0 +1 @@ +pub mod button; diff --git a/crates/frontend/src/components/condition_pills/condition_pills.rs b/crates/frontend/src/components/condition_pills/condition_pills.rs new file mode 100644 index 000000000..5c180e6e4 --- /dev/null +++ b/crates/frontend/src/components/condition_pills/condition_pills.rs @@ -0,0 +1,64 @@ +use super::utils::{extract_and_format, parse_conditions}; +use leptos::*; +use serde_json::Value; + +#[component] +pub fn context_pills(context: Value) -> impl IntoView { + let condition = extract_and_format(&context); + let ctx_values = parse_conditions(condition.clone()); + + view! { + {ctx_values + .into_iter() + .map(|condition| { + let dimension = condition.left_operand; + let op = condition.operator; + let val = condition.right_operand; + let operator = op.clone(); + view! { + <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs ring-1 ring-inset ring-purple-700/10 shadow-md gap-x-2"> + <span class="font-mono font-medium context_condition text-gray-500"> + {dimension} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {op} + </span> + + {match operator.trim() { + "BETWEEN" => { + let split_val: Vec<String> = val + .clone() + .split(",") + .map(String::from) + .collect(); + view! { + <> + <span class="font-mono font-semibold context_condition"> + {&split_val[0]} + </span> + <span class="font-mono font-medium text-gray-650 context_condition "> + {"and"} + </span> + <span class="font-mono font-semibold context_condition"> + {&split_val[1]} + </span> + </> + } + } + _ => { + view! { + <> + <span class="font-mono font-semibold context_condition"> + {val} + </span> + </> + } + } + }} + + </span> + } + }) + .collect::<Vec<_>>()} + } +} diff --git a/crates/frontend/src/components/condition_pills/mod.rs b/crates/frontend/src/components/condition_pills/mod.rs new file mode 100644 index 000000000..2343919bf --- /dev/null +++ b/crates/frontend/src/components/condition_pills/mod.rs @@ -0,0 +1,3 @@ +pub mod condition_pills; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/condition_pills/types.rs b/crates/frontend/src/components/condition_pills/types.rs new file mode 100644 index 000000000..05681bb18 --- /dev/null +++ b/crates/frontend/src/components/condition_pills/types.rs @@ -0,0 +1,6 @@ +#[derive(Clone)] +pub struct Condition { + pub left_operand: String, + pub operator: String, + pub right_operand: String, +} diff --git a/crates/frontend/src/components/condition_pills/utils.rs b/crates/frontend/src/components/condition_pills/utils.rs new file mode 100644 index 000000000..bd6f93ffa --- /dev/null +++ b/crates/frontend/src/components/condition_pills/utils.rs @@ -0,0 +1,135 @@ +use std::mem::swap; + +use super::types::Condition; +use serde_json::Value; + +pub fn parse_conditions(input: Vec<Condition>) -> Vec<Condition> { + let mut conditions = Vec::new(); + + // Split the string by "&&" and iterate over each condition + for condition in input { + let mut key = condition.left_operand; + let mut op = condition.operator; + let mut val = condition.right_operand; + if op == "in" { + swap(&mut key, &mut val) + } + // Add a space after key + key.push(' '); + match op.as_str() { + "==" => { + val = val.trim_matches('"').to_string(); + op = "is".to_string(); + } + "<=" => { + val = val.trim_matches('"').to_string(); + op = "BETWEEN".to_string(); + } + _ => { + val = val.trim_matches('"').to_string(); + op = "has".to_string(); + } + } + op.push(' '); + + conditions.push(Condition { + left_operand: key, + operator: op, + right_operand: val, + }); + } + + conditions +} + +pub fn extract_and_format(condition: &Value) -> Vec<Condition> { + let mut formatted_conditions = Vec::new(); + if condition.is_object() && condition.get("and").is_some() { + // Handling complex "and" conditions + let empty_vec = vec![]; + let conditions_json = condition + .get("and") + .and_then(|val| val.as_array()) + .unwrap_or(&empty_vec); // Default to an empty vector if not an array + + for cond in conditions_json { + if let Some(formatted_condition) = format_condition(cond) { + formatted_conditions.push(formatted_condition); + } + } + } else if let Some(formatted_condition) = format_condition(condition) { + // Handling single conditions + formatted_conditions.push(formatted_condition); + } + formatted_conditions +} + +fn format_condition(condition: &Value) -> Option<Condition> { + if let Some(ref operator) = condition.as_object().and_then(|obj| obj.keys().next()) { + let empty_vec = vec![]; + let operands = condition[operator].as_array().unwrap_or(&empty_vec); + + // Handling the "in" operator differently + if operator.as_str() == "in" { + let left_operand = &operands[0]; + let right_operand = &operands[1]; + + let left_str = if left_operand.is_string() { + format!("\"{}\"", left_operand.as_str().unwrap()) + } else { + format!("{}", left_operand) + }; + + if right_operand.is_object() && right_operand["var"].is_string() { + let var_str = right_operand["var"].as_str().unwrap(); + return Some(Condition { + left_operand: left_str, + operator: operator.to_string(), + right_operand: var_str.to_string(), + }); + } + } + + // Handline the "<=" operator differently + if operator.as_str() == "<=" { + let left_operand = &operands[0]; + let right_operand = &operands[2]; + let mid_operand = &operands[1]; + + let left_str = format!("{}", left_operand).trim_matches('"').to_string(); + let right_str = format!("{}", right_operand).trim_matches('"').to_string(); + + if mid_operand.is_object() && mid_operand["var"].is_string() { + let var_str = mid_operand["var"].as_str().unwrap(); + return Some(Condition { + left_operand: var_str.to_string(), + operator: operator.to_string(), + right_operand: left_str + "," + &right_str, + }); + } + } + // Handling regular operators + if let Some(first_operand) = operands.get(0) { + if first_operand.is_object() && first_operand["var"].is_string() { + let key = first_operand["var"] + .as_str() + .unwrap_or("UnknownVar") + .to_string(); + if let Some(value) = operands.get(1) { + let val_str = match value { + Value::Null => "null".to_string(), + Value::String(v) => v.clone(), + _ => value.to_string(), + }; + return Some(Condition { + left_operand: key.to_string(), + operator: operator.to_string(), + right_operand: val_str, + }); + } + } + } + } + + None +} diff --git a/crates/frontend/src/components/context_form/context_form.rs b/crates/frontend/src/components/context_form/context_form.rs new file mode 100644 index 000000000..d454fdb72 --- /dev/null +++ b/crates/frontend/src/components/context_form/context_form.rs @@ -0,0 +1,234 @@ +use crate::components::dropdown::dropdown::{Dropdown, DropdownDirection}; +use crate::types::Dimension; +use leptos::*; +use std::collections::HashSet; +use web_sys::MouseEvent; + +#[component] +pub fn context_form<NF>( + handle_change: NF, + dimensions: Vec<Dimension>, + is_standalone: bool, + context: Vec<(String, String, String)>, + #[prop(default = String::new())] heading_sub_text: String, + #[prop(default = false)] disabled: bool, + #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, + #[prop(default = false)] resolve_mode: bool, +) -> impl IntoView +where + NF: Fn(Vec<(String, String, String)>) + 'static, +{ + let _has_dimensions = dimensions.len() > 0; + + let (context, set_context) = create_signal(context.clone()); + let (used_dimensions, set_used_dimensions) = create_signal(HashSet::new()); + + let dimensions = StoredValue::new(dimensions); + + let last_idx = create_memo(move |_| context.get().len().max(1) - 1); + + let on_click = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Context form submit"); + //TODO: submit logic for this + }; + + create_effect(move |_| { + let f_context = context.get(); + handle_change(f_context.clone()); + }); + + let handle_select_dropdown_option = move |selected_dimension: Dimension| { + let dimension_name = selected_dimension.dimension; + set_context.update(|value| { + leptos::logging::log!("{:?}", value); + value.push((dimension_name.to_string(), "".to_string(), "".to_string())) + }); + set_used_dimensions.update(|value: &mut HashSet<String>| { + value.insert(dimension_name.to_string()); + }); + }; + + view! { + <div> + <div class="form-control w-full "> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> + <span class="label-text font-semibold text-base">Context</span> + <span class="label-text text-slate-400">{heading_sub_text}</span> + </label> + </div> + <div class="card w-full bg-slate-50"> + <div class="card-body"> + <Show when=move || context.get().len() == 0> + <div class="flex justify-center"> + <Dropdown + dropdown_width="w-80" + dropdown_icon="ri-add-line".to_string() + dropdown_text="Add Context".to_string() + dropdown_direction + dropdown_options=dimensions.get_value() + disabled=disabled + on_select=Box::new(handle_select_dropdown_option) + /> + </div> + </Show> + <For + each=move || { + context + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, String, String))>>() + } + + key=|(idx, (dimension, _, _))| format!("{}-{}", dimension, idx) + children=move |(idx, (dimension, operator, value))| { + let dimension_label = dimension.to_string(); + let dimension_name = dimension.to_string(); + view! { + <div class="flex gap-x-6"> + <div class="form-control"> + <label class="label font-mono text-sm"> + <span class="label-text">Dimension</span> + </label> + <input + value=dimension_label + class="input w-full max-w-xs" + name="context-dimension-name" + disabled=true + /> + </div> + <div class="form-control w-20"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">Operator</span> + </label> + + <select + disabled=disabled || resolve_mode + value=operator.clone() + on:input=move |event| { + let input_value = event_target_value(&event); + set_context + .update(|curr_context| { + curr_context[idx].1 = input_value; + }); + } + + name="context-dimension-operator" + class="select select-bordered w-full max-w-xs text-sm rounded-lg h-10 px-4 appearance-none leading-tight focus:outline-none focus:shadow-outline" + > + <option disabled selected=!resolve_mode> + Pick one + </option> + <option + value="==" + selected=operator.clone() == "==" || resolve_mode + > + "IS" + </option> + <option value="IN" selected=operator.clone() == "IN"> + "HAS" + </option> + <option value="<=" selected=operator.clone() == "<="> + "BETWEEN (inclusive)" + </option> + </select> + + </div> + <div class="form-control"> + <label class="label font-mono text-sm"> + <span class="label-text">Value</span> + </label> + <div class="flex gap-x-6 items-center"> + <input + disabled=disabled + value=value + on:input=move |event| { + let input_value = event_target_value(&event); + set_context + .update(|curr_context| { + curr_context[idx].2 = input_value; + }); + } + + name="context-dimension-value" + type="text" + placeholder="Type here" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + /> + <button + class="btn btn-ghost btn-circle btn-sm" + disabled=disabled + on:click=move |_| { + let mut current_context = context.get(); + current_context.remove(idx); + set_context.set(current_context); + set_used_dimensions + .update(|value| { + value.remove(&dimension_name); + }); + } + > + + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> + </div> + </div> + </div> + + {move || { + if last_idx.get() != idx { + view! { + <div class="my-3 ml-5 ml-6 ml-7"> + <span class="font-mono text-xs">"&&"</span> + </div> + } + .into_view() + } else { + view! {}.into_view() + } + }} + } + } + /> + + <Show when=move || context.get().len() != 0> + <div class="mt-4"> + + {move || { + let dimensions = dimensions + .get_value() + .into_iter() + .filter(|dimension| { + !used_dimensions.get().contains(&dimension.dimension) + }) + .collect::<Vec<Dimension>>(); + view! { + <Dropdown + dropdown_icon="ri-add-line".to_string() + dropdown_text="Add Context".to_string() + dropdown_options=dimensions + disabled=disabled + dropdown_direction + on_select=Box::new(handle_select_dropdown_option) + /> + } + }} + + </div> + </Show> + + </div> + </div> + </div> + <Show when=move || is_standalone> + <div class="flex justify-end"> + <button class="btn" on:click:undelegated=on_click disabled=disabled> + Save + </button> + </div> + </Show> + </div> + } +} diff --git a/crates/frontend/src/components/context_form/mod.rs b/crates/frontend/src/components/context_form/mod.rs new file mode 100644 index 000000000..6f9238576 --- /dev/null +++ b/crates/frontend/src/components/context_form/mod.rs @@ -0,0 +1,2 @@ +pub mod context_form; +pub mod utils; diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs new file mode 100644 index 000000000..f68d30c54 --- /dev/null +++ b/crates/frontend/src/components/context_form/utils.rs @@ -0,0 +1,118 @@ +use crate::types::Dimension; +use crate::utils::{get_config_value, get_host, ConfigType}; +use anyhow::Result; +use reqwest::StatusCode; +use serde_json::{json, Map, Value}; + +pub fn get_condition_schema( + var: &str, + op: &str, + val: &str, + dimensions: Vec<Dimension>, +) -> Result<Value, String> { + match op { + "<=" => { + let mut split_value = val.split(','); + + let first_operand = + split_value.next().unwrap().trim().parse::<i64>().unwrap(); + + let dimension_val = get_config_value( + var, + split_value.next().unwrap().trim(), + &dimensions + .into_iter() + .map(ConfigType::Dimension) + .collect::<Vec<_>>(), + ); + + Ok(json!({ + op: [ + first_operand, + { "var": var }, + dimension_val.expect("can't parse dimension value") + ] + })) + } + _ => { + let dimension_val = get_config_value( + var, + val, + &dimensions + .into_iter() + .map(ConfigType::Dimension) + .collect::<Vec<_>>(), + ); + Ok(json!({ + op: [ + {"var": var}, + dimension_val.expect("can't parse dimension value") + ] + })) + } + } +} + +pub fn construct_context( + conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, +) -> Value { + let condition_schemas = conditions + .iter() + .map(|(variable, operator, value)| { + get_condition_schema(variable, operator, value, dimensions.clone()).unwrap() + }) + .collect::<Vec<Value>>(); + + let context = if condition_schemas.len() == 1 { + condition_schemas[0].clone() + } else { + json!({ "and": condition_schemas }) + }; + + context +} + +pub fn construct_request_payload( + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, +) -> Value { + // Construct the override section + let override_section: Map<String, Value> = overrides; + + // Construct the context section + let context_section = construct_context(conditions, dimensions); + + // Construct the entire request payload + let request_payload = json!({ + "override": override_section, + "context": context_section + }); + + request_payload +} + +pub async fn create_context( + tenant: String, + overrides: Map<String, Value>, + conditions: Vec<(String, String, String)>, + dimensions: Vec<Dimension>, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/context"); + let request_payload = construct_request_payload(overrides, conditions, dimensions); + let response = client + .put(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/default_config_form/default_config_form.rs b/crates/frontend/src/components/default_config_form/default_config_form.rs new file mode 100644 index 000000000..4f94b4bc7 --- /dev/null +++ b/crates/frontend/src/components/default_config_form/default_config_form.rs @@ -0,0 +1,375 @@ +use leptos::*; +use serde_json::{json, Number, Value}; +use std::str::FromStr; +use web_sys::MouseEvent; + +use crate::{ + api::fetch_functions, + components::{ + button::button::Button, + dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + }, + types::FunctionsName, + utils::parse_string_to_json_value_vec, +}; + +use super::{types::DefaultConfigCreateReq, utils::create_default_config}; + +#[component] +pub fn default_config_form<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] config_key: String, + #[prop(default = String::new())] config_type: String, + #[prop(default = String::new())] config_pattern: String, + #[prop(default = String::new())] config_value: String, + #[prop(default = None)] function_name: Option<Value>, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let (config_key, set_config_key) = create_signal(config_key); + let (config_type, set_config_type) = create_signal(config_type); + let (config_pattern, set_config_pattern) = create_signal(config_pattern); + let (config_value, set_config_value) = create_signal(config_value); + let (function_name, set_function_name) = create_signal(function_name); + + let functions_resource: Resource<String, Vec<crate::types::FunctionResponse>> = + create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_functions(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let handle_select_dropdown_option = move |selected_function: FunctionsName| { + set_function_name.update(|value| { + let function_name = selected_function.clone(); + leptos::logging::log!("function selected: {:?}", function_name); + let fun_name = match function_name.as_str() { + "None" => None, + _ => Some(json!(function_name)), + }; + *value = fun_name; + }); + }; + + let (show_labels, set_show_labels) = create_signal(edit); + + let (error_message, set_error_message) = create_signal("".to_string()); + + let on_submit = move |ev: MouseEvent| { + ev.prevent_default(); + let f_name = config_key.get(); + let f_type = config_type.get(); + let f_pattern = config_pattern.get(); + let f_value = config_value.get(); + let fun_name = function_name.get(); + + let f_value = match f_type.as_str() { + "number" => Value::Number(f_value.parse::<i64>().unwrap().into()), + "decimal" => match f64::from_str(&f_value) { + Ok(num) => match Number::from_f64(num) { + Some(number) => Value::Number(number), + None => Value::String( + "Invalid decimal format or precision issue".to_string(), + ), + }, + Err(_) => Value::String("Invalid decimal format".to_string()), + }, + "boolean" => match bool::from_str(&f_value) { + Ok(boolean) => Value::Bool(boolean), + _ => Value::String("Invalid Boolean".to_string()), + }, + "pattern" | "enum" => Value::String(f_value), + _ => Value::from_str(&f_value).expect("Error parsing JSON"), + }; + + let f_schema = match f_type.as_str() { + "number" => { + json!({ + "type": f_type.to_string(), + }) + } + "decimal" => { + json!({ + "type": "number".to_string(), + }) + } + "boolean" => { + json!({ + "type": "boolean".to_string(), + } + ) + } + "enum" => { + json!({ + "type": "string", + "enum": parse_string_to_json_value_vec(f_pattern.as_str()) + }) + } + "pattern" => { + json!({ + "type": "string", + "pattern": f_pattern.to_string() + }) + } + _ => Value::from_str(&f_pattern).expect("Error parsing JSON"), + }; + + let payload = DefaultConfigCreateReq { + schema: f_schema, + value: f_value, + function_name: fun_name, + }; + + let handle_submit_clone = handle_submit.clone(); + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_default_config( + f_name.clone(), + tenant_rs.get(), + payload.clone(), + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(e) => { + set_error_message.set(e); + // Handle error + // Consider logging or displaying the error + } + } + } + }); + }; + view! { + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> + <div class="form-control"> + <label class="label"> + <span class="label-text">Key Name</span> + </label> + <input + disabled=edit + type="text" + placeholder="Key" + class="input input-bordered w-full max-w-md" + value=config_key.get() + on:change=move |ev| { + let value = event_target_value(&ev); + set_config_key.set(value); + } + /> + + </div> + + <div class="divider"></div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Set Schema</span> + </label> + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_config_type.set("number".to_string()); + } + "decimal" => { + set_config_type.set("decimal".to_string()); + } + "boolean" => { + set_config_type.set("boolean".to_string()); + } + "enum" => { + set_config_type.set("enum".to_string()); + set_config_pattern.set(format!("{:?}", vec!["android", "web", "ios"])); + } + "pattern" => { + set_config_type.set("pattern".to_string()); + set_config_pattern.set(".*".to_string()); + } + _ => { + set_config_type.set("other".to_string()); + set_config_pattern.set("".to_string()); + } + }; + } + + class="select select-bordered w-full max-w-md" + > + <option disabled selected> + Choose Schema Type + </option> + + <option + value="number" + selected=move || { config_type.get() == "number".to_string() } + > + "Number" + </option> + <option + value="decimal" + selected=move || { config_type.get() == "decimal".to_string() } + > + "Decimal (16 digits)" + </option> + <option + value="boolean" + selected=move || { config_type.get() == "boolean".to_string() } + > + "Boolean" + </option> + <option value="enum" selected=move || { config_type.get() == "enum".to_string() }> + "String (Enum)" + </option> + <option + value="pattern" + selected=move || { config_type.get() == "pattern".to_string() } + > + "String (regex)" + </option> + <option value="other" selected=move || { config_type.get() == "other".to_string() }> + "Other" + </option> + </select> + </div> + + <div class="divider"></div> + + {move || { + view! { + <Show when=move || { + (config_type.get() == "number") || (config_type.get() == "decimal") + }> + <div class="form-control"> + <label class="label"> + <span class="label-text">Value</span> + </label> + <input + type="number" + placeholder="Value" + class="input input-bordered w-full max-w-md" + value=config_value.get() + on:change=move |ev| { + logging::log!("{:?}", event_target_value(& ev)); + set_config_value.set(event_target_value(&ev)); + } + /> + + </div> + <div class="divider"></div> + </Show> + + <Show when=move || { show_labels.get() && (config_type.get() != "number") + && (config_type.get() != "decimal") + }> + <div class="form-control"> + <label class="label"> + <span class="label-text ">Value</span> + </label> + <input + type="text" + placeholder="Value" + class="input input-bordered w-full max-w-md" + value=config_value.get() + on:change=move |ev| { + logging::log!("{:?}", event_target_value(& ev)); + set_config_value.set(event_target_value(&ev)); + } + /> + + </div> + + <div class="divider"></div> + + <Show when=move || (config_type.get() != "boolean")> + <div class="form-control"> + <label class="label"> + <span class="label-text"> + {config_type.get()} + </span> + </label> + <textarea + type="text" + class="input input-bordered w-full max-w-md pt-[10px]" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_config_pattern.set(value); + } + > + + {config_pattern.get()} + </textarea> + </div> + <div class="divider"></div> + </Show> + + </Show> + } + }} + + <Suspense> + {move || { + let functions = functions_resource.get().unwrap_or(vec![]); + let mut function_names: Vec<FunctionsName> = vec![]; + functions.into_iter().for_each(|ele| { + function_names.push(ele.function_name); + }); + function_names.sort(); + function_names.insert(0, "None".to_string()); + view! { + <div class="form-control"> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> + <span class="label-text">Function Name</span> + <span class="label-text text-slate-400">Assign Function validation to your key</span> + </label> + </div> + + <div class="mt-2"> + <Dropdown + dropdown_width="w-100" + dropdown_icon="".to_string() + dropdown_text={function_name.get().and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }).map_or("Add Function".to_string(), |v| v.to_string())} + dropdown_direction=DropdownDirection::Down + dropdown_btn_type=DropdownBtnType::Select + dropdown_options=function_names + on_select=Box::new(handle_select_dropdown_option) + /> + </ div> + </ div> + } + }} + </ Suspense> + + <div class="form-control grid w-full justify-end"> + <Button class="pl-[70px] pr-[70px]".to_string() text="Submit".to_string() on_click=on_submit/> + </div> + + { + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } + + </form> + } +} diff --git a/crates/frontend/src/components/default_config_form/mod.rs b/crates/frontend/src/components/default_config_form/mod.rs new file mode 100644 index 000000000..8a08759dc --- /dev/null +++ b/crates/frontend/src/components/default_config_form/mod.rs @@ -0,0 +1,3 @@ +pub mod default_config_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/default_config_form/types.rs b/crates/frontend/src/components/default_config_form/types.rs new file mode 100644 index 000000000..3a86a7bcf --- /dev/null +++ b/crates/frontend/src/components/default_config_form/types.rs @@ -0,0 +1,9 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize, Clone)] +pub struct DefaultConfigCreateReq { + pub schema: Value, + pub value: Value, + pub function_name: Option<Value>, +} diff --git a/crates/frontend/src/components/default_config_form/utils.rs b/crates/frontend/src/components/default_config_form/utils.rs new file mode 100644 index 000000000..32f492913 --- /dev/null +++ b/crates/frontend/src/components/default_config_form/utils.rs @@ -0,0 +1,31 @@ +use super::types::DefaultConfigCreateReq; +use crate::utils::get_host; +use reqwest::StatusCode; + +pub async fn create_default_config( + key: String, + tenant: String, + payload: DefaultConfigCreateReq, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/default-config/{key}"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .json(&payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK | StatusCode::CREATED => { + response.text().await.map_err(|e| e.to_string()) + } + StatusCode::BAD_REQUEST => Err(response + .text() + .await + .unwrap_or("Validation of configuration value failed, but the error could not be understood by the system. Contact an admin for help if this persists".to_string())), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs new file mode 100644 index 000000000..cd9114184 --- /dev/null +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -0,0 +1,333 @@ +use super::types::DimensionCreateReq; +use super::utils::create_dimension; +use crate::components::dropdown::dropdown::{ + Dropdown, DropdownBtnType, DropdownDirection, +}; +use crate::types::FunctionsName; +use crate::utils::parse_string_to_json_value_vec; +use crate::{api::fetch_functions, components::button::button::Button}; +use leptos::*; +use serde_json::{json, Value}; +use std::str::FromStr; +use web_sys::MouseEvent; + +#[component] +pub fn dimension_form<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = 0)] priority: u16, + #[prop(default = String::new())] dimension_name: String, + #[prop(default = String::new())] dimension_type: String, + #[prop(default = String::new())] dimension_pattern: String, + #[prop(default = None)] function_name: Option<Value>, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let (priority, set_priority) = create_signal(priority); + let (dimension_name, set_dimension_name) = create_signal(dimension_name); + let (dimension_type, set_dimension_type) = create_signal(dimension_type); + let (dimension_pattern, set_dimension_pattern) = create_signal(dimension_pattern); + + let (function_name, set_function_name) = create_signal(function_name); + + let functions_resource: Resource<String, Vec<crate::types::FunctionResponse>> = + create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_functions(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let handle_select_dropdown_option = move |selected_function: FunctionsName| { + set_function_name.update(|value| { + let function_name = selected_function.clone(); + leptos::logging::log!("function selected: {:?}", function_name); + let fun_name = match function_name.as_str() { + "None" => None, + _ => Some(json!(function_name)), + }; + *value = fun_name; + }); + }; + + let (show_labels, set_show_labels) = create_signal(edit); + + let (error_message, set_error_message) = create_signal("".to_string()); + + let on_submit = move |ev: MouseEvent| { + ev.prevent_default(); + let f_priority = priority.get(); + let f_name = dimension_name.get(); + let f_type = dimension_type.get(); + let f_pattern = dimension_pattern.get(); + let fun_name = function_name.get(); + + let f_schema = match f_type.as_str() { + "number" => { + json!({ + "type": f_type.to_string() + }) + } + "decimal" => { + json!({ + "type": "number".to_string(), + }) + } + "boolean" => { + json!({ + "type": "boolean".to_string(), + } + ) + } + "enum" => { + json!({ + "type": "string", + "enum": parse_string_to_json_value_vec(f_pattern.as_str()) + }) + } + "pattern" => { + json!({ + "type": "string", + "pattern": f_pattern.to_string() + }) + } + _ => Value::from_str(&f_pattern).expect("Error parsing JSON"), + }; + + let payload = DimensionCreateReq { + dimension: f_name, + priority: f_priority, + schema: f_schema, + function_name: fun_name, + }; + + let handle_submit_clone = handle_submit.clone(); + spawn_local({ + let handle_submit = handle_submit_clone; + async move { + let result = create_dimension(tenant_rs.get(), payload.clone()).await; + + match result { + Ok(_) => { + handle_submit(); + } + Err(e) => { + set_error_message.set(e); + // Handle error + // Consider logging or displaying the error + } + } + } + }); + }; + view! { + <form class="form-control w-full space-y-4 bg-white text-gray-700 font-mono"> + <div class="form-control"> + <label class="label"> + <span class="label-text">Dimension</span> + </label> + <input + disabled=edit + type="text" + placeholder="Dimension" + class="input input-bordered w-full max-w-md" + value=dimension_name.get() + on:change=move |ev| { + let value = event_target_value(&ev); + set_dimension_name.set(value); + } + /> + + </div> + + <div class="divider"></div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Set Schema</span> + </label> + <select + name="schemaType[]" + on:change=move |ev| { + set_show_labels.set(true); + match event_target_value(&ev).as_str() { + "number" => { + set_dimension_type.set("number".to_string()); + } + "decimal" => { + set_dimension_type.set("decimal".to_string()); + } + "boolean" => { + set_dimension_type.set("boolean".to_string()); + } + "enum" => { + set_dimension_type.set("enum".to_string()); + set_dimension_pattern + .set(format!("{:?}", vec!["android", "web", "ios"])); + } + "pattern" => { + set_dimension_type.set("pattern".to_string()); + set_dimension_pattern.set(".*".to_string()); + } + _ => { + set_dimension_type.set("other".to_string()); + set_dimension_pattern.set("".to_string()); + } + }; + } + + class="select select-bordered w-full max-w-md" + > + <option disabled selected> + Set Schema + </option> + + <option + value="number" + selected=move || { dimension_type.get() == "number".to_string() } + > + + "Number" + </option> + <option + value="decimal" + selected= move || {dimension_type.get() == "decimal".to_string()} + > + "Decimal (Max Value : 1.7976931348623157e+308)" + </option> + <option + value= "boolean" + selected= move || {dimension_type.get() == "boolean".to_string()} + > + "Boolean" + </option> + <option + value="enum" + selected=move || { dimension_type.get() == "enum".to_string() } + > + "String (Enum)" + </option> + <option + value="pattern" + selected=move || { dimension_type.get() == "pattern".to_string() } + > + "String (regex)" + </option> + <option + value="other" + selected=move || { dimension_type.get() == "other".to_string() } + > + "Other" + </option> + </select> + </div> + + <div class="divider"></div> + + {move || { + view! { + <div class="form-control"> + <label class="label"> + <span class="label-text">Priority</span> + </label> + <input + type="Number" + placeholder="Priority" + class="input input-bordered w-full max-w-md" + value=priority.get() + on:change=move |ev| { + logging::log!( + "{:?}", event_target_value(& ev).parse::< u16 > () + ); + match event_target_value(&ev).parse::<u16>() { + Ok(i_prio) => set_priority.set(i_prio), + Err(e) => logging::log!("{e}"), + }; + } + /> + + </div> + + <Show when=move || (show_labels.get() && ((dimension_type.get() == "enum") || (dimension_type.get() == "pattern") || (dimension_type.get() == "other")))> + <div class="form-control"> + <label class="label font-mono"> + <span class="label-text text-gray-700 font-mono"> + {dimension_type.get()} + </span> + </label> + <textarea + type="text" + class="input input-bordered w-full max-w-md pt-[10px]" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_dimension_pattern.set(value); + } + > + + {dimension_pattern.get()} + </textarea> + + </div> + <div class="divider"></div> + </Show> + } + }} + + <Suspense> + {move || { + let mut functions = functions_resource.get().unwrap_or(vec![]); + let mut function_names: Vec<FunctionsName> = vec!["None".to_string()]; + functions.sort_by(|a, b| a.function_name.cmp(&b.function_name)); + functions.into_iter().for_each(|ele| { + function_names.push(ele.function_name); + }); + view! { + <div class="form-control"> + <div class="gap-1"> + <label class="label flex-col justify-center items-start"> + <span class="label-text">Function Name</span> + <span class="label-text text-slate-400">Assign Function validation to your key</span> + </label> + </div> + + <div class="mt-2"> + <Dropdown + dropdown_width="w-100" + dropdown_icon="".to_string() + dropdown_text={function_name.get().and_then(|v| match v { + Value::String(s) => Some(s), + _ => None, + }).map_or("Add Function".to_string(), |v| v.to_string())} + dropdown_direction=DropdownDirection::Down + dropdown_btn_type=DropdownBtnType::Select + dropdown_options=function_names + on_select=Box::new(handle_select_dropdown_option) + /> + </ div> + </ div> + } + }} + </ Suspense> + + <div class="form-control grid w-full justify-end"> + <Button class="pl-[70px] pr-[70px]".to_string() text="Submit".to_string() on_click=on_submit/> + </div> + + { + view! { + <div> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + } + } + + </form> + } +} diff --git a/crates/frontend/src/components/dimension_form/mod.rs b/crates/frontend/src/components/dimension_form/mod.rs new file mode 100644 index 000000000..3c93972ac --- /dev/null +++ b/crates/frontend/src/components/dimension_form/mod.rs @@ -0,0 +1,3 @@ +pub mod dimension_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/dimension_form/types.rs b/crates/frontend/src/components/dimension_form/types.rs new file mode 100644 index 000000000..e4e60e1ac --- /dev/null +++ b/crates/frontend/src/components/dimension_form/types.rs @@ -0,0 +1,10 @@ +use serde::Serialize; +use serde_json::Value; + +#[derive(Serialize, Clone)] +pub struct DimensionCreateReq { + pub dimension: String, + pub priority: u16, + pub schema: Value, + pub function_name: Option<Value>, +} diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs new file mode 100644 index 000000000..c17cffe9a --- /dev/null +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -0,0 +1,26 @@ +use super::types::DimensionCreateReq; +use crate::utils::get_host; +use reqwest::StatusCode; + +pub async fn create_dimension( + tenant: String, + payload: DimensionCreateReq, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/dimension"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .json(&payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/drawer/drawer.rs b/crates/frontend/src/components/drawer/drawer.rs new file mode 100644 index 000000000..a4be54c54 --- /dev/null +++ b/crates/frontend/src/components/drawer/drawer.rs @@ -0,0 +1,79 @@ +use leptos::*; + +use crate::utils::get_element_by_id; + +pub fn open_drawer(id: &str) { + match get_element_by_id::<web_sys::HtmlInputElement>(id) { + Some(ele) => ele.set_checked(true), + None => { + logging::log!("{} drawer checkbox not found", id); + } + }; +} + +pub fn close_drawer(id: &str) { + match get_element_by_id::<web_sys::HtmlInputElement>(id) { + Some(ele) => ele.set_checked(false), + None => { + logging::log!("{} drawer checkbox not found", id); + } + }; +} + +#[component] +pub fn drawer_btn(drawer_id: String, children: Children) -> impl IntoView { + let open_drawer_id = drawer_id.clone(); + view! { + <button + class=format!( + "btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 drawer-button", + ) + + id=format!("{}-btn", drawer_id.clone()) + on:click=move |_| { open_drawer(&open_drawer_id) } + > + {children()} + </button> + } +} + +#[component] +pub fn drawer<NF>( + id: String, + children: Children, + #[prop(default = "")] header: &'static str, + #[prop(default = "w-[60vw]")] drawer_width: &'static str, + handle_close: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let close_drawer = move |_| { + handle_close(); + }; + + view! { + <div class="drawer drawer-end"> + <input id=id.clone() type="checkbox" class="drawer-toggle"/> + + <div class="drawer-side drawer-zindex w-full"> + <label for=id.clone() class="drawer-overlay" on:click=close_drawer.clone()></label> + <div class=format!( + "min-h-full {drawer_width} bg-base-100 overflow-x-hidden overflow-y-auto", + )> + <div class="px-4 py-4 flex justify-between items-center"> + <h3 class="text-lg font-bold">{header}</h3> + <button + class="btn btn-sm btn-circle btn-ghost" + on:click=close_drawer + > + <i class="ri-close-line"></i> + </button> + </div> + <div class="divider mt-0"></div> + <div class="p-4">{children()}</div> + </div> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/drawer/mod.rs b/crates/frontend/src/components/drawer/mod.rs new file mode 100644 index 000000000..150dae562 --- /dev/null +++ b/crates/frontend/src/components/drawer/mod.rs @@ -0,0 +1 @@ +pub mod drawer; diff --git a/crates/frontend/src/components/dropdown/dropdown.rs b/crates/frontend/src/components/dropdown/dropdown.rs new file mode 100644 index 000000000..0bb733b52 --- /dev/null +++ b/crates/frontend/src/components/dropdown/dropdown.rs @@ -0,0 +1,122 @@ +use leptos::*; + +use super::utils::DropdownOption; + +#[derive(PartialEq)] +pub enum DropdownBtnType { + Outline, + Link, + Fill, + Select, +} + +#[derive(PartialEq, Copy, Clone)] +pub enum DropdownDirection { + Right, + Left, + Top, + Down, +} + +#[component] +pub fn dropdown<T>( + dropdown_text: String, + dropdown_options: Vec<T>, + on_select: Box<dyn Fn(T)>, + #[prop(default = "".to_string())] dropdown_icon: String, + #[prop(default = DropdownDirection::Right)] dropdown_direction: DropdownDirection, + #[prop(default = DropdownBtnType::Outline)] dropdown_btn_type: DropdownBtnType, + #[prop(default = "w-96")] dropdown_width: &'static str, + #[prop(default = false)] disabled: bool, + #[prop(default = true)] searchable: bool, +) -> impl IntoView +where + T: DropdownOption + Clone + 'static, +{ + let all_options = StoredValue::new(dropdown_options.clone()); + let (search_term, set_search_term) = create_signal(String::new()); + let dropdown_options = Signal::derive(move || { + let term = search_term.get(); + all_options + .get_value() + .into_iter() + .filter(|option| option.label().contains(&term)) + .collect::<Vec<T>>() + }); + let on_select = StoredValue::new(on_select); + + let btn_class = match dropdown_btn_type { + DropdownBtnType::Outline => "btn btn-sm text-xs m-1 w-full btn-purple-outline", + DropdownBtnType::Link => "btn btn-sm text-xs m-1 w-full btn-purple-link", + DropdownBtnType::Fill => "btn btn-sm text-xs m-1 w-full btn-purple-fill", + DropdownBtnType::Select => "select select-bordered w-[28rem] items-center", + }; + + view! { + <div + class="dropdown" + class=("disable-click", disabled) + class=("dropdown-right", dropdown_direction == DropdownDirection::Right) + class=("dropdown-left", dropdown_direction == DropdownDirection::Left) + class=("dropdown-top", dropdown_direction == DropdownDirection::Top) + class=("dropdown-down", dropdown_direction == DropdownDirection::Down) + > + <label tabindex="0" class=btn_class> + <i class=format!("{dropdown_icon}")></i> + {dropdown_text} + </label> + <ul + tabindex="0" + class=format!( + "{dropdown_width} dropdown-content z-[1] menu flex-nowrap p-2 shadow bg-base-100 rounded-box max-h-96 overflow-y-scroll overflow-x-hidden", + ) + > + + {move || { + if searchable { + view! { + <div class="mb-3"> + <label class="input input-bordered flex items-center gap-2 h-10"> + <i class="ri-search-line"></i> + <input + type="text" + class="grow" + placeholder="Search" + on:input=move |event| { + set_search_term.set(event_target_value(&event)); + } + /> + + </label> + </div> + } + .into_view() + } else { + view! {}.into_view() + } + }} + + <For + each=move || dropdown_options.get() + key=|option: &T| option.key() + children=move |option: T| { + let label = option.label(); + view! { + <li + class="w-full" + on:click=move |_| { + let selected_option = option.clone(); + on_select.with_value(|f| f(selected_option)); + } + > + + <a class="w-full word-break-break">{label.to_string()}</a> + </li> + } + } + /> + + </ul> + </div> + } +} diff --git a/crates/frontend/src/components/dropdown/mod.rs b/crates/frontend/src/components/dropdown/mod.rs new file mode 100644 index 000000000..53d6d0629 --- /dev/null +++ b/crates/frontend/src/components/dropdown/mod.rs @@ -0,0 +1,2 @@ +pub mod dropdown; +pub mod utils; diff --git a/crates/frontend/src/components/dropdown/utils.rs b/crates/frontend/src/components/dropdown/utils.rs new file mode 100644 index 000000000..e39b36bf9 --- /dev/null +++ b/crates/frontend/src/components/dropdown/utils.rs @@ -0,0 +1,4 @@ +pub trait DropdownOption { + fn key(&self) -> String; + fn label(&self) -> String; +} diff --git a/crates/frontend/src/components/experiment/experiment.rs b/crates/frontend/src/components/experiment/experiment.rs new file mode 100644 index 000000000..12d067e31 --- /dev/null +++ b/crates/frontend/src/components/experiment/experiment.rs @@ -0,0 +1,203 @@ +use std::rc::Rc; + +use leptos::*; + +use crate::components::condition_pills::utils::extract_and_format; +use crate::components::table::table::Table; + +use super::utils::gen_variant_table; +use crate::types::{Experiment, ExperimentStatusType}; + +#[component] +pub fn experiment<HS, HR, HC, HE>( + experiment: Experiment, + handle_start: HS, + handle_ramp: HR, + handle_conclude: HC, + handle_edit: HE, +) -> impl IntoView +where + HS: Fn(String) + 'static + Clone, + HR: Fn() + 'static + Clone, + HC: Fn() + 'static + Clone, + HE: Fn() + 'static + Clone, +{ + let experiment_rc = Rc::new(experiment.clone()); + let contexts = extract_and_format(&experiment_rc.clone().context); + + view! { + <div class="flex flex-col overflow-x-auto p-2 bg-transparent"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let class_name = match exp.status { + ExperimentStatusType::CREATED => { + "badge text-white ml-3 mb-1 badge-xl badge-info" + } + ExperimentStatusType::INPROGRESS => { + "badge text-white ml-3 mb-1 badge-xl badge-warning" + } + ExperimentStatusType::CONCLUDED => { + "badge text-white ml-3 mb-1 badge-xl badge-success" + } + }; + view! { + <h1 class="text-2xl pt-4 font-extrabold"> + {&exp.name} <span class=class_name>{exp.status.to_string()}</span> + </h1> + } + } + } + <div class="divider"></div> + <div class="flex flex-row justify-end join m-5"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let handle_start = handle_start.clone(); + let handle_conclude = handle_conclude.clone(); + let handle_ramp = handle_ramp.clone(); + let handle_edit = handle_edit.clone(); + match exp.status { + ExperimentStatusType::CREATED => { + view! { + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_edit() } + > + + <i class="ri-edit-line"></i> + Edit + </button> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_start(exp.id.to_string()) } + > + + <i class="ri-guide-line"></i> + Start + </button> + } + .into_view() + } + ExperimentStatusType::INPROGRESS => { + view! { + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_conclude() } + > + + <i class="ri-stop-circle-line"></i> + Conclude + </button> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { handle_ramp() } + > + + <i class="ri-flight-takeoff-line"></i> + Ramp + </button> + } + .into_view() + } + ExperimentStatusType::CONCLUDED => { + view! { + <div class="stat"> + <div class="stat-title">Chosen Variant</div> + <div class="stat-value"> + {match exp.chosen_variant { + Some(ref v) => format!("{}", v), + None => String::new(), + }} + + </div> + </div> + } + .into_view() + } + } + } + } + + </div> + <div class="flex bg-base-100 flex-row gap-2 justify-between flex-wrap shadow m-5"> + <div class="stat w-2/12"> + <div class="stat-title">Experiment ID</div> + <div class="stat-value text-sm">{experiment.id}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Current Traffic Percentage</div> + <div class="stat-value text-sm">{experiment.traffic_percentage}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Created by</div> + <div class="stat-value text-sm">{experiment.created_by}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Created at</div> + <div class="stat-value text-sm"> + {format!("{}", experiment.created_at.format("%v"))} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Last Modified</div> + <div class="stat-value text-sm"> + + {format!("{}", experiment.last_modified.format("%v"))} + + </div> + </div> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> + <div class="card-body"> + <h2 class="card-title">Context</h2> + <div class="flex flex-row flex-wrap gap-2"> + {move || { + let mut view = Vec::new(); + for token in contexts.clone() { + let (dimension, value) = (token.left_operand, token.right_operand); + view.push( + view! { + <div class="stat w-3/12"> + <div class="stat-title">{dimension}</div> + <div class="stat-value text-base">{&value.replace("\"", "")} + </div> + </div> + }, + ); + } + view + }} + + </div> + </div> + </div> <div class="card bg-base-100 max-w-screen shadow m-5"> + <div class="card-body"> + <h2 class="card-title">Variants</h2> + <div class="overflow-x-auto overflow-y-auto"> + + { + let experiment_clone = experiment_rc.clone(); + move || { + let exp = experiment_clone.clone(); + let (rows, columns) = gen_variant_table(&exp.variants).unwrap(); + view! { + <Table + cell_style="min-w-48 font-mono".to_string() + rows=rows + key_column="overrides".to_string() + columns=columns + /> + } + } + } + + </div> + </div> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/experiment/mod.rs b/crates/frontend/src/components/experiment/mod.rs new file mode 100644 index 000000000..780b31f34 --- /dev/null +++ b/crates/frontend/src/components/experiment/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment; +pub mod utils; diff --git a/crates/frontend/src/components/experiment/utils.rs b/crates/frontend/src/components/experiment/utils.rs new file mode 100644 index 000000000..47d3d0915 --- /dev/null +++ b/crates/frontend/src/components/experiment/utils.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use crate::{ + components::table::types::Column, + types::{Variant, VariantType}, +}; +use leptos::{view, IntoView}; +use serde_json::{Map, Value}; + +pub fn gen_variant_table( + variants: &[Variant], +) -> Result<(Vec<Map<String, Value>>, Vec<Column>), String> { + let mut columns = vec![Column::default("Config Key".into())]; + let mut row_map: HashMap<&String, Map<String, Value>> = HashMap::new(); + for (i, variant) in variants.iter().enumerate() { + let name = match variant.variant_type { + VariantType::CONTROL => format!("{}", variant.variant_type), + VariantType::EXPERIMENTAL => format!("Variant-{}", i), + }; + columns.push(Column::new(name.clone(), None, |value: &str, _| { + view! { <span>{value.to_string()}</span> }.into_view() + })); + for (config, value) in variant.overrides.iter() { + match row_map.get_mut(config) { + Some(c) => { + c.insert(name.clone(), value.clone()); + } + None => { + let mut m = Map::new(); + m.insert("Config Key".into(), Value::String(config.clone())); + m.insert(name.clone(), value.clone()); + row_map.insert(config, m); + } + } + } + } + let rows = row_map.into_values().collect(); + Ok((rows, columns)) +} diff --git a/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs b/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs new file mode 100644 index 000000000..0453bb4ac --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/experiment_conclude_form.rs @@ -0,0 +1,95 @@ +use std::rc::Rc; + +use super::utils::conclude_experiment; +use crate::types::{Experiment, Variant, VariantType}; +use leptos::*; + +#[component] +pub fn experiment_conclude_form<HS>( + experiment: Experiment, + handle_submit: HS, +) -> impl IntoView +where + HS: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let experiment_rc = Rc::new(experiment); + + let experiment_clone = experiment_rc.clone(); + let handle_conclude_experiment = move |variant_id: String| { + let handle_submit_clone = handle_submit.clone(); + spawn_local(async move { + let experiment = experiment_clone.clone(); + let tenant = tenant_rs.get(); + let _ = + conclude_experiment(experiment.id.to_string(), variant_id, &tenant).await; + handle_submit_clone(); + }) + }; + + view! { + <h3 class="font-bold text-lg">Conclude This Experiment</h3> + <p class="py-4"> + Choose a variant to conclude with, this variant becomes + the new default that is served to requests that match this context + </p> + <form method="dialog"> + <For + each=move || { + experiment_rc + .clone() + .variants + .clone() + .into_iter() + .enumerate() + .collect::<Vec<(usize, Variant)>>() + } + + key=|(_, variant)| variant.id.to_string() + children=move |(idx, variant)| { + let variant = variant.clone(); + let variant_type = variant.variant_type; + let variant_id = variant.id; + let handle_conclude_experiment_clone = handle_conclude_experiment.clone(); + match variant_type { + VariantType::CONTROL => { + view! { + <button + class="btn btn-block btn-outline btn-info m-2" + on:click=move |_| { + let handle_conclude_experiment_clone = handle_conclude_experiment_clone + .clone(); + handle_conclude_experiment_clone(variant_id.to_string()) + } + > + + Control + </button> + } + .into_view() + } + VariantType::EXPERIMENTAL => { + { + view! { + <button + class="btn btn-block btn-outline btn-success m-2" + on:click=move |_| { + let handle_conclude_experiment_clone = handle_conclude_experiment_clone + .clone(); + handle_conclude_experiment_clone(variant_id.to_string()) + } + > + + {format!("Variant-{idx}")} + </button> + } + } + .into_view() + } + } + } + /> + + </form> + } +} diff --git a/crates/frontend/src/components/experiment_conclude_form/mod.rs b/crates/frontend/src/components/experiment_conclude_form/mod.rs new file mode 100644 index 000000000..1205d1162 --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_conclude_form; +pub mod utils; diff --git a/crates/frontend/src/components/experiment_conclude_form/utils.rs b/crates/frontend/src/components/experiment_conclude_form/utils.rs new file mode 100644 index 000000000..b73829c95 --- /dev/null +++ b/crates/frontend/src/components/experiment_conclude_form/utils.rs @@ -0,0 +1,29 @@ +use leptos::logging::log; +use serde_json::json; + +use crate::{types::Experiment, utils::get_host}; + +pub async fn conclude_experiment( + exp_id: String, + variant_id: String, + tenant: &String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + let host = get_host(); + match client + .patch(format!("{host}/experiments/{}/conclude", exp_id)) + .header("x-tenant", tenant) + .json(&json!({ "chosen_variant": variant_id })) + .send() + .await + { + Ok(experiment) => { + log!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} diff --git a/crates/frontend/src/components/experiment_form/experiment_form.rs b/crates/frontend/src/components/experiment_form/experiment_form.rs new file mode 100644 index 000000000..e860fe9d6 --- /dev/null +++ b/crates/frontend/src/components/experiment_form/experiment_form.rs @@ -0,0 +1,184 @@ +use super::utils::{create_experiment, update_experiment}; +use crate::components::button::button::Button; +use crate::components::context_form::context_form::ContextForm; +use crate::components::variant_form::variant_form::VariantForm; +use crate::types::{DefaultConfig, Dimension, Variant, VariantType}; +use leptos::*; +use serde_json::Map; +use web_sys::MouseEvent; + +fn default_variants_for_form() -> Vec<(String, Variant)> { + vec![ + ( + "control-variant".to_string(), + Variant { + id: "control".to_string(), + variant_type: VariantType::CONTROL, + context_id: None, + override_id: None, + overrides: Map::new(), + }, + ), + ( + "experimental-variant".to_string(), + Variant { + id: "experimental".to_string(), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: Map::new(), + }, + ), + ] +} + +fn get_init_state(variants: &[Variant]) -> Vec<(String, Variant)> { + let init_variants = if variants.len() == 0 { + default_variants_for_form() + } else { + variants + .into_iter() + .map(|variant| (variant.id.to_string(), variant.clone())) + .collect::<Vec<(String, Variant)>>() + }; + + init_variants +} + +#[component] +pub fn experiment_form<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] id: String, + name: String, + context: Vec<(String, String, String)>, + variants: Vec<Variant>, + handle_submit: NF, + default_config: Vec<DefaultConfig>, + dimensions: Vec<Dimension>, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let init_variants = get_init_state(&variants); + let default_config = StoredValue::new(default_config); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let (experiment_name, set_experiment_name) = create_signal(name); + let (f_context, set_context) = create_signal(context.clone()); + let (f_variants, set_variants) = create_signal(init_variants); + + let handle_context_form_change = move |updated_ctx: Vec<(String, String, String)>| { + set_context.set_untracked(updated_ctx); + }; + + let handle_variant_form_change = move |updated_varaints: Vec<(String, Variant)>| { + set_variants.set_untracked(updated_varaints); + }; + + let dimensions = StoredValue::new(dimensions); + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting experiment form"); + logging::log!("{:?}", f_variants.get()); + + let f_experiment_name = experiment_name.get(); + let f_context = f_context.get(); + let f_variants = f_variants + .get() + .into_iter() + .map(|(_, variant)| variant) + .collect::<Vec<Variant>>(); + let tenant = tenant_rs.get(); + let experiment_id = id.clone(); + let handle_submit_clone = handle_submit.clone(); + + logging::log!("{:?}", f_experiment_name); + logging::log!("{:?}", f_context); + + spawn_local({ + async move { + let result = if edit { + update_experiment(experiment_id, f_variants, tenant).await + } else { + create_experiment( + f_context, + f_variants, + f_experiment_name, + tenant, + dimensions.get_value(), + ) + .await + }; + + match result { + Ok(_) => { + handle_submit_clone(); + } + Err(_) => { + // Handle error + // We can consider logging or displaying the error + } + } + } + }); + }; + + view! { + <div> + <div class="form-control w-full"> + <label class="label"> + <span class="label-text">Experiment Name</span> + </label> + <input + disabled=edit + value=move || experiment_name.get() + on:input=move |ev| set_experiment_name.set(event_target_value(&ev)) + type="text" + name="expName" + id="expName" + placeholder="ex: testing hyperpay release" + class="input input-bordered w-full max-w-md" + /> + </div> + + <div class="divider"></div> + + <div class="my-4"> + {move || { + let context = f_context.get(); + view! { + <ContextForm + dimensions=dimensions.get_value() + context=context + handle_change=handle_context_form_change + is_standalone=false + disabled=edit + heading_sub_text=String::from( + "Define rules under which this experiment would run", + ) + /> + } + }} + + </div> + + <div class="divider"></div> + + {move || { + let variants = f_variants.get(); + view! { + <VariantForm + edit=edit + variants=variants + default_config=default_config.get_value() + handle_change=handle_variant_form_change + /> + } + }} + + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/experiment_form/mod.rs b/crates/frontend/src/components/experiment_form/mod.rs new file mode 100644 index 000000000..ccf974665 --- /dev/null +++ b/crates/frontend/src/components/experiment_form/mod.rs @@ -0,0 +1,3 @@ +pub mod experiment_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/experiment_form/types.rs b/crates/frontend/src/components/experiment_form/types.rs new file mode 100644 index 000000000..8efc20222 --- /dev/null +++ b/crates/frontend/src/components/experiment_form/types.rs @@ -0,0 +1,22 @@ +use crate::types::Variant; +use serde::Serialize; +use serde_json::{Map, Value}; + +#[derive(Serialize)] +pub struct ExperimentCreateRequest { + pub name: String, + + pub context: Value, + pub variants: Vec<Variant>, +} + +#[derive(Serialize, Debug)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: Map<String, Value>, +} + +#[derive(Serialize, Debug)] +pub struct ExperimentUpdateRequest { + pub variants: Vec<VariantUpdateRequest>, +} diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs new file mode 100644 index 000000000..cc1f661ed --- /dev/null +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -0,0 +1,83 @@ +use super::types::{ + ExperimentCreateRequest, ExperimentUpdateRequest, VariantUpdateRequest, +}; +use crate::components::context_form::utils::construct_context; +use crate::types::{Dimension, Variant}; +use crate::utils::get_host; +use reqwest::StatusCode; +use serde_json::json; + +pub fn validate_experiment(experiment: &ExperimentCreateRequest) -> Result<bool, String> { + if experiment.name.is_empty() { + return Err(String::from("experiment name should not be empty")); + } + Ok(true) +} + +pub async fn create_experiment( + conditions: Vec<(String, String, String)>, + variants: Vec<Variant>, + name: String, + tenant: String, + dimensions: Vec<Dimension>, +) -> Result<String, String> { + let payload = ExperimentCreateRequest { + name, + variants, + context: construct_context(conditions, dimensions), + }; + + let _ = validate_experiment(&payload)?; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/experiments"); + let request_payload = json!(payload); + let response = client + .post(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} + +pub async fn update_experiment( + experiment_id: String, + variants: Vec<Variant>, + tenant: String, +) -> Result<String, String> { + let payload = ExperimentUpdateRequest { + variants: variants + .into_iter() + .map(|variant| VariantUpdateRequest { + id: variant.id, + overrides: variant.overrides, + }) + .collect::<Vec<VariantUpdateRequest>>(), + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{}/experiments/{}/overrides", host, experiment_id); + let request_payload = json!(payload); + let response = client + .put(url) + .header("x-tenant", tenant) + .header("Authorization", "Bearer 12345678") + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + match response.status() { + StatusCode::OK => response.text().await.map_err(|e| e.to_string()), + StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), + _ => Err("Internal Server Error".to_string()), + } +} diff --git a/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs b/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs new file mode 100644 index 000000000..539113855 --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/experiment_ramp_form.rs @@ -0,0 +1,53 @@ +use std::rc::Rc; + +use leptos::{logging::log, *}; +use web_sys::MouseEvent; + +use super::utils::ramp_experiment; +use crate::{components::button::button::Button, types::Experiment}; + +#[component] +pub fn experiment_ramp_form<NF>( + experiment: Experiment, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let (traffic, set_traffic) = create_signal(experiment.traffic_percentage); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let experiment_rc = Rc::new(experiment); + let handle_ramp_experiment = move |event: MouseEvent| { + event.prevent_default(); + let experiment_clone = experiment_rc.clone(); + let handle_submit_clone = handle_submit.clone(); + spawn_local(async move { + let tenant = tenant_rs.get(); + let traffic_value = traffic.get(); + let _ = ramp_experiment(&experiment_clone.id, traffic_value, &tenant).await; + handle_submit_clone() + }); + }; + view! { + <h3 class="font-bold text-lg">Ramp up with release</h3> + <p class="py-4">Increase the traffic being redirected to the variants</p> + <form> + <p>{move || traffic.get()}</p> + <input + type="range" + min="0" + max="100" + value=move || traffic.get() + class="range" + on:input=move |event| { + let traffic_value = event_target_value(&event).parse::<u8>().unwrap(); + log!("traffic value:{traffic_value}"); + set_traffic.set(traffic_value); + } + /> + + <Button text="Set".to_string() on_click=handle_ramp_experiment/> + </form> + } +} diff --git a/crates/frontend/src/components/experiment_ramp_form/mod.rs b/crates/frontend/src/components/experiment_ramp_form/mod.rs new file mode 100644 index 000000000..706bb7d5d --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_ramp_form; +pub mod utils; diff --git a/crates/frontend/src/components/experiment_ramp_form/utils.rs b/crates/frontend/src/components/experiment_ramp_form/utils.rs new file mode 100644 index 000000000..7018ad619 --- /dev/null +++ b/crates/frontend/src/components/experiment_ramp_form/utils.rs @@ -0,0 +1,29 @@ +use leptos::logging::log; +use serde_json::json; + +use crate::{types::Experiment, utils::get_host}; + +pub async fn ramp_experiment( + exp_id: &String, + percent: u8, + tenant: &String, +) -> Result<Experiment, String> { + let client = reqwest::Client::new(); + let host = get_host(); + match client + .patch(format!("{host}/experiments/{}/ramp", exp_id)) + .header("x-tenant", tenant) + .json(&json!({ "traffic_percentage": percent })) + .send() + .await + { + Ok(experiment) => { + log!("experiment response {:?}", experiment); + Ok(experiment + .json::<Experiment>() + .await + .map_err(|err| err.to_string())?) + } + Err(e) => Err(e.to_string()), + } +} diff --git a/crates/frontend/src/components/function_form/function_form.rs b/crates/frontend/src/components/function_form/function_form.rs new file mode 100644 index 000000000..cfcb36f3c --- /dev/null +++ b/crates/frontend/src/components/function_form/function_form.rs @@ -0,0 +1,291 @@ +use super::utils::{create_function, test_function, update_function}; +use crate::components::button::button::Button; +use leptos::*; +use serde_json::{from_str, json, Value}; +use web_sys::MouseEvent; + +#[component] +pub fn function_editor<NF>( + #[prop(default = false)] edit: bool, + #[prop(default = String::new())] function_name: String, + #[prop(default = String::new())] function: String, + #[prop(default = String::new())] runtime_version: String, + #[prop(default = String::new())] description: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (function_name, set_function_name) = create_signal(function_name); + let (function, set_function) = create_signal(function); + let (runtime_version, set_runtime_version) = create_signal(runtime_version); + let (error_message, set_error_message) = create_signal("".to_string()); + let (description, set_description) = create_signal(description); + if !edit { + set_runtime_version.set("1.0.0".to_string()) + }; + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + + let tenant = tenant_rs.get(); + let f_function_name = function_name.get(); + let f_function = function.get(); + let f_runtime_version = runtime_version.get(); + let f_description = description.get(); + let handle_submit_clone = handle_submit.clone(); + + logging::log!("Function Name in editor: {:?}", function_name); + + spawn_local({ + async move { + let result = if edit == true { + update_function( + f_function_name, + f_function, + f_runtime_version, + f_description, + tenant, + ) + .await + } else { + create_function( + f_function_name, + f_function, + f_runtime_version, + f_description, + tenant, + ) + .await + }; + + match result { + Ok(_) => { + handle_submit_clone(); + } + Err(e) => { + set_error_message.set(e); + } + } + } + }); + }; + + view! { + <div> + <form id="MyForm"> + + <div class="flex flex-row w-full justify-between"> + <div class="form-group"> + + <div + class="monaco" + id="function" + style="min-height: 500px; min-width: 1000px" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("Function editor - Function Name: {:?}", value); + set_function.set(value); + } + ></div> + </div> + + <div class="mx-auto w-auto" style="width: 250px"> + + <Show when=move || { edit == false }> + <div class="form-control "> + <label class="label"> + <span class="label-text">Function Name</span> + </label> + <input + disabled=edit + value=move || function_name.get() + on:input=move |ev| { + set_function_name.set(event_target_value(&ev)) + } + type="text" + name="funName" + id="funName" + placeholder="ex: myfunction" + class="input input-bordered w-full max-w-md" + /> + </div> + </Show> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Draft Runtime Version</span> + </label> + <input + disabled=true + value=move || runtime_version.get() + on:input=move |ev| set_runtime_version.set(event_target_value(&ev)) + type="text" + name="runVersion" + id="runVersion" + placeholder="Js Runtime Version" + class="input input-bordered " + /> + </div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Description</span> + </label> + <textarea + type="text" + class="input input-bordered shadow-md" + name="description" + id="description" + placeholder="explain function" + on:change=move |ev| { + let value = event_target_value(&ev); + logging::log!("{:?}", value); + set_description.set(value); + } + > + + {description.get()} + </textarea> + </div> + + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + <div class="flex"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + </div> + + </div> + </form> + </div> + } +} + +#[component] +pub fn test_form<NF>( + function_name: String, + stage: String, + handle_submit: NF, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (error_message, set_error_message) = create_signal("".to_string()); + let (output_message, set_output_message) = create_signal("".to_string()); + let (val, set_val) = create_signal(json!({})); + let (key, set_key) = create_signal("".to_string()); + + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + + let tenant = tenant_rs.get(); + let f_function_name = function_name.clone(); + let f_val = json!({ + "key": key.get(), + "value": val.get() + }); + let f_stage = stage.clone(); + + logging::log!("{:?}", function_name); + logging::log!("{:?}", val); + + spawn_local({ + async move { + let result = test_function(f_function_name, f_stage, f_val, tenant).await; + + match result { + Ok(resp) => { + set_error_message.set("".to_string()); + set_output_message.set(resp); + } + Err(e) => { + set_output_message.set("".to_string()); + set_error_message.set(e); + } + } + } + }); + }; + + view! { + <div class="flex flex-row justify-between"> + + <div class="monaco" style="min-height: 500px; min-width: 1000px"></div> + + <div class="mx-auto w-auto" style="width: 250px"> + + <form id="MyForm"> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Key Name</span> + </label> + <input + disabled=false + value=move || key.get() + on:input=move |ev| set_key.set(event_target_value(&ev)) + type="text" + name="key name" + id="keyName" + placeholder="key" + class="input input-bordered" + /> + </div> + + <div class="form-control"> + <label class="label"> + <span class="label-text">Value</span> + </label> + <textarea + type="text" + class="input input-bordered shadow-md" + name="value" + id="value" + style="min-height: 150px" + placeholder="value" + on:change=move |ev| { + let value = event_target_value(&ev); + match from_str::<Value>(&value) { + Ok(test_val) => { + set_val.set(test_val); + set_error_message.set("".to_string()); + set_output_message.set("".to_string()); + } + Err(_) => { + set_val.set(json!(value)); + set_error_message.set("".to_string()); + set_output_message.set("".to_string()); + + } + }; + } + > + + {"".to_string()} + </textarea> + + </div> + + <div class="flex justify-end mt-8"> + <Button text="Submit".to_string() on_click=on_submit/> + </div> + + <div class="mt-7"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + + <div> + <p class="text-red-500">{move || output_message.get()}</p> + </div> + + </form> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/function_form/mod.rs b/crates/frontend/src/components/function_form/mod.rs new file mode 100644 index 000000000..fb05a0779 --- /dev/null +++ b/crates/frontend/src/components/function_form/mod.rs @@ -0,0 +1,3 @@ +pub mod function_form; +pub mod types; +pub mod utils; diff --git a/crates/frontend/src/components/function_form/types.rs b/crates/frontend/src/components/function_form/types.rs new file mode 100644 index 000000000..f4fe58975 --- /dev/null +++ b/crates/frontend/src/components/function_form/types.rs @@ -0,0 +1,16 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct FunctionCreateRequest { + pub function_name: String, + pub function: String, + pub runtime_version: String, + pub description: String, +} + +#[derive(Serialize)] +pub struct FunctionUpdateRequest { + pub function: String, + pub runtime_version: String, + pub description: String, +} diff --git a/crates/frontend/src/components/function_form/utils.rs b/crates/frontend/src/components/function_form/utils.rs new file mode 100644 index 000000000..5e006cc04 --- /dev/null +++ b/crates/frontend/src/components/function_form/utils.rs @@ -0,0 +1,107 @@ +use super::types::{FunctionCreateRequest, FunctionUpdateRequest}; +use crate::utils::get_host; +use reqwest::StatusCode; +use serde_json::{json, Value}; + +pub async fn create_function( + function_name: String, + function: String, + runtime_version: String, + description: String, + tenant: String, +) -> Result<String, String> { + let payload = FunctionCreateRequest { + function_name, + function, + runtime_version, + description, + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function"); + let request_payload = json!(payload); + let response = client + .post(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} + +pub async fn update_function( + function_name: String, + function: String, + runtime_version: String, + description: String, + tenant: String, +) -> Result<String, String> { + let payload = FunctionUpdateRequest { + function, + runtime_version, + description, + }; + + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}"); + let request_payload = json!(payload); + + let response = client + .patch(url) + .header("x-tenant", tenant) + .json(&request_payload) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} + +pub async fn test_function( + function_name: String, + stage: String, + val: Value, + tenant: String, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}/{stage}/test"); + + let response = client + .put(url) + .header("x-tenant", tenant) + .json(&val) + .send() + .await + .map_err(|e| e.to_string())?; + + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} diff --git a/crates/frontend/src/components/mod.rs b/crates/frontend/src/components/mod.rs new file mode 100644 index 000000000..55c922460 --- /dev/null +++ b/crates/frontend/src/components/mod.rs @@ -0,0 +1,20 @@ +pub mod button; +pub mod condition_pills; +pub mod context_form; +pub mod default_config_form; +pub mod dimension_form; +pub mod drawer; +pub mod dropdown; +pub mod experiment; +pub mod experiment_conclude_form; +pub mod experiment_form; +pub mod experiment_ramp_form; +pub mod function_form; +pub mod modal; +pub mod nav_item; +pub mod override_form; +pub mod pagination; +pub mod side_nav; +pub mod stat; +pub mod table; +pub mod variant_form; diff --git a/crates/frontend/src/components/modal/mod.rs b/crates/frontend/src/components/modal/mod.rs new file mode 100644 index 000000000..6738a0f2f --- /dev/null +++ b/crates/frontend/src/components/modal/mod.rs @@ -0,0 +1 @@ +pub mod modal; diff --git a/crates/frontend/src/components/modal/modal.rs b/crates/frontend/src/components/modal/modal.rs new file mode 100644 index 000000000..b2dd46da3 --- /dev/null +++ b/crates/frontend/src/components/modal/modal.rs @@ -0,0 +1,30 @@ +use leptos::*; + +#[component] +pub fn modal<NF>( + id: String, + #[prop(default = String::new())] classnames: String, + #[prop(default = String::new())] heading: String, + handle_close: NF, + children: Children, +) -> impl IntoView +where + NF: Fn() + 'static + Clone, +{ + let classnames = format!("modal-box {classnames}"); + view! { + <dialog id=id class="modal modal-middle"> + <div class=classnames> + <button + class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" + on:click=move |_| { handle_close() } + > + <i class="ri-close-line"></i> + </button> + <h3 class="font-bold text-lg">{heading}</h3> + {children()} + <div class="modal-action"></div> + </div> + </dialog> + } +} diff --git a/crates/frontend/src/components/nav_item/mod.rs b/crates/frontend/src/components/nav_item/mod.rs new file mode 100644 index 000000000..96edba6b5 --- /dev/null +++ b/crates/frontend/src/components/nav_item/mod.rs @@ -0,0 +1 @@ +pub mod nav_item; diff --git a/crates/frontend/src/components/nav_item/nav_item.rs b/crates/frontend/src/components/nav_item/nav_item.rs new file mode 100644 index 000000000..33a132f6d --- /dev/null +++ b/crates/frontend/src/components/nav_item/nav_item.rs @@ -0,0 +1,34 @@ +use leptos::*; +use leptos_router::A; + +#[component] +pub fn nav_item( + is_active: bool, + href: String, + text: String, + icon: String, +) -> impl IntoView { + let (anchor_class, icon_wrapper_class, icon_class) = if is_active { + ( + "py-2.5 px-4 flex items-center whitespace-nowrap active".to_string(), + "rounded-lg text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none + focus:ring-purple-300 dark:focus:ring-purple-800 shadow-lg shadow-purple-500/50 dark:shadow-lg dark:shadow-purple-800/80 w-8 h-8 flex content-center justify-center pt-0.5 px-1 ".to_string(), + format!("{} text-lg text-white font-normal", icon) + ) + } else { + ( + "py-2.5 px-4 flex items-center whitespace-nowrap".to_string(), + "rounded-lg shadow-xl shadow-slate-300 bg-white w-8 h-8 flex content-center justify-center pt-0.5 px-1".to_string(), + format!("{} text-lg text-purple-800 font-normal", icon) + ) + }; + + view! { + <A href=href class=anchor_class> + <div class=icon_wrapper_class> + <i class=icon_class></i> + </div> + <span class="ml-1 duration-300 opacity-100 pointer-events-none ease-soft">{text}</span> + </A> + } +} diff --git a/crates/frontend/src/components/override_form/mod.rs b/crates/frontend/src/components/override_form/mod.rs new file mode 100644 index 000000000..3c1de112d --- /dev/null +++ b/crates/frontend/src/components/override_form/mod.rs @@ -0,0 +1 @@ +pub mod override_form; diff --git a/crates/frontend/src/components/override_form/override_form.rs b/crates/frontend/src/components/override_form/override_form.rs new file mode 100644 index 000000000..ca064aec9 --- /dev/null +++ b/crates/frontend/src/components/override_form/override_form.rs @@ -0,0 +1,165 @@ +use crate::{ + components::dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + types::DefaultConfig, + utils::{get_config_value, ConfigType}, +}; +use leptos::*; +use serde_json::{json, Map, Value}; +use web_sys::MouseEvent; + +#[component] +pub fn override_form<NF>( + overrides: Map<String, Value>, + default_config: Vec<DefaultConfig>, + handle_change: NF, + is_standalone: bool, + #[prop(default = false)] disable_remove: bool, + #[prop(default = true)] show_add_override: bool, + #[prop(into, default = None)] handle_key_remove: Option<Callback<String, ()>>, +) -> impl IntoView +where + NF: Fn(Map<String, Value>) + 'static, +{ + let default_config = StoredValue::new(default_config); + let (overrides, set_overrides) = create_signal(overrides.clone()); + let unused_config_keys = Signal::derive(move || { + default_config + .get_value() + .into_iter() + .filter(|config| !overrides.get().contains_key(&config.key)) + .collect::<Vec<DefaultConfig>>() + }); + // let has_default_config = Signal::derive(move || unused_config_keys.get().len() > 0); + + let on_submit = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("{:?}", overrides.get()); + }; + + let handle_config_key_select = move |default_config: DefaultConfig| { + let config_key = default_config.key; + set_overrides.update(|value| { + value.insert(config_key.to_string(), json!("")); + }); + }; + + create_effect(move |_| { + let f_override = overrides.get(); + handle_change(f_override.clone()); + }); + + view! { + <div> + <div class="space-y-4"> + <div class="flex items-center justify-between gap-4"> + <label class="label"> + <span class="label-text font-semibold text-base">Overrides</span> + </label> + <Show when=move || show_add_override> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + </Show> + </div> + <Show when=move || overrides.get().len() == 0> + <div class="p-4 text-gray-400 flex flex-col justify-center items-center"> + <div> + <i class="ri-add-circle-line text-xl"></i> + </div> + <div> + <span class="text-semibold text-sm">Add Override</span> + </div> + </div> + </Show> + <For + each=move || { overrides.get().into_iter().collect::<Vec<(String, Value)>>() } + key=|(config_key, _)| config_key.to_string() + children=move |(config_key, config_value)| { + let config_key_label = config_key.to_string(); + let config_key_value = config_key.to_string(); + let config_value = config_value.to_string().replace("\"", ""); + view! { + <div> + <div class="flex items-center gap-4"> + <div class="form-control"> + <label class="label font-medium font-mono text-sm"> + <span class="label-text">{config_key_label} ":"</span> + </label> + </div> + <div class="form-control w-2/5"> + <input + type="text" + placeholder="Enter override here" + name="override" + class="input input-bordered w-full bg-white text-gray-700 shadow-md" + value=config_value + on:input=move |event| { + let input_value = event_target_value(&event); + let default_config_val = get_config_value( + &config_key_value, + &input_value, + &default_config.get_value().into_iter().map(ConfigType::DefaultConfig).collect::<Vec<_>>(), + ).expect("can't parse default config key"); + set_overrides + .update(|curr_overrides| { + curr_overrides + .insert( + config_key_value.to_string(), + json!(default_config_val), + ); + }); + } + /> + + </div> + <div class="w-1/5"> + + {if !disable_remove { + view! { + <button + class="btn btn-ghost btn-circle btn-sm" + on:click=move |ev| { + ev.prevent_default(); + match handle_key_remove { + Some(f) => f.call(config_key.clone()), + None => { + set_overrides + .update(|value| { + value.remove(&config_key); + }) + } + }; + } + > + + <i class="ri-delete-bin-2-line text-xl text-2xl font-bold"></i> + </button> + } + .into_view() + } else { + view! {}.into_view() + }} + + </div> + </div> + </div> + } + } + /> + + </div> + <Show when=move || is_standalone> + <div class="flex justify-end"> + <button class="btn" on:click:undelegated=on_submit> + Save + </button> + </div> + </Show> + </div> + } +} diff --git a/crates/frontend/src/components/pagination/mod.rs b/crates/frontend/src/components/pagination/mod.rs new file mode 100644 index 000000000..bc8665b62 --- /dev/null +++ b/crates/frontend/src/components/pagination/mod.rs @@ -0,0 +1 @@ +pub mod pagination; diff --git a/crates/frontend/src/components/pagination/pagination.rs b/crates/frontend/src/components/pagination/pagination.rs new file mode 100644 index 000000000..c8f4db0df --- /dev/null +++ b/crates/frontend/src/components/pagination/pagination.rs @@ -0,0 +1,27 @@ +use leptos::*; + +#[component] +pub fn pagination<NF, PF>( + current_page: i64, + total_pages: i64, + next: NF, + previous: PF, +) -> impl IntoView +where + NF: Fn() + 'static, + PF: Fn() + 'static, +{ + view! { + <div class="join"> + <button class="join-item btn" on:click=move |_| previous()> + "«" + </button> + <button class="join-item btn"> + {format!("Page {} / {}", current_page, total_pages)} + </button> + <button class="join-item btn" on:click=move |_| next()> + "»" + </button> + </div> + } +} diff --git a/crates/frontend/src/components/side_nav/mod.rs b/crates/frontend/src/components/side_nav/mod.rs new file mode 100644 index 000000000..14f93bf3c --- /dev/null +++ b/crates/frontend/src/components/side_nav/mod.rs @@ -0,0 +1 @@ +pub mod side_nav; diff --git a/crates/frontend/src/components/side_nav/side_nav.rs b/crates/frontend/src/components/side_nav/side_nav.rs new file mode 100644 index 000000000..42cc051b4 --- /dev/null +++ b/crates/frontend/src/components/side_nav/side_nav.rs @@ -0,0 +1,172 @@ +use crate::components::nav_item::nav_item::NavItem; +use crate::types::AppRoute; +use crate::utils::{get_tenants, use_url_base}; + +use leptos::*; +use leptos_router::{use_location, use_navigate, A}; +use web_sys::Event; + +fn create_routes(tenant: &str) -> Vec<AppRoute> { + let base = use_url_base(); + vec![ + AppRoute { + key: format!("{base}/admin/{tenant}/experiments"), + path: format!("{base}/admin/{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"), + icon: "ri-code-box-fill".to_string(), + label: "Functions".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/dimensions"), + path: format!("{base}/admin/{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"), + icon: "ri-tools-line".to_string(), + label: "Default Config".to_string(), + }, + AppRoute { + key: format!("{base}/admin/{tenant}/overrides"), + path: format!("{base}/admin/{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"), + icon: "ri-equalizer-fill".to_string(), + label: "Resolve".to_string(), + }, + ] +} + +#[component] +pub fn side_nav( + resolved_path: String, + original_path: String, + //params_map: Memo<ParamsMap>, +) -> impl IntoView { + let location = use_location(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let tenant_ws = use_context::<WriteSignal<String>>().unwrap(); + let (app_routes, set_app_routes) = + create_signal(create_routes(tenant_rs.get().as_str())); + + let resolved_path = create_rw_signal(resolved_path); + let original_path = create_rw_signal(original_path); + + create_effect(move |_| { + let current_path = location.pathname.get(); + + set_app_routes.update(|app_routes| { + for route in app_routes { + if current_path.contains(&route.path) { + route.key = format!("{}-{}", route.path, "active"); + } else { + route.key = route.path.to_string(); + } + } + }) + }); + + view! { + <div class="max-w-xs z-990 fixed my-4 ml-4 block w-full h-full flex-wrap inset-y-0 items-center justify-between overflow-y-auto rounded-2xl border-0 bg-white p-0 shadow-none -translate-x-full transition-transform duration-200 xl:left-0 xl:translate-x-0 xl:bg-transparent"> + <div class="h-19.5"> + <A + href="/admin" + class="block px-8 py-6 m-0 text-sm whitespace-nowrap text-slate-700" + > + <span class="ml-1 font-semibold transition-all duration-200"> + Superposition Platform + </span> + </A> + </div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + <select + value=tenant_rs.get() + on:change=move |event: Event| { + let selected_tenant = event_target_value(&event); + let base = use_url_base(); + let resolved_path_c = resolved_path.get().replace(&base, ""); + let original_path_c = original_path.get().replace(&base, ""); + logging::log!("ORIGINAL_PATH: {:?}", original_path_c); + let redirect_url = std::iter::zip( + original_path_c.split("/"), + resolved_path_c.split("/"), + ) + .map(|(o_token, r_token)| match o_token { + ":tenant" => selected_tenant.clone(), + _ => r_token.to_string(), + }) + .collect::<Vec<String>>() + .join("/"); + tenant_ws.set(selected_tenant.clone()); + set_app_routes.set(create_routes(selected_tenant.as_str())); + let navigate = use_navigate(); + navigate(redirect_url.as_str(), Default::default()) + } + + class="select w-full max-w-xs shadow-md" + > + + {move || { + let tenants = get_tenants(); + match tenants.is_empty() { + false => { + tenants + .iter() + .map(|tenant| { + view! { + <option selected=tenant + == &tenant_rs.get()>{tenant}</option> + } + }) + .collect::<Vec<_>>() + } + true => { + vec![ + view! { <option disabled=true>{"Loading tenants..."}</option> }, + ] + } + } + }} + + </select> + // <hr class="h-px mt-0 mb-1 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent"/> + <div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full"> + <ul class="menu"> + <For + each=move || app_routes.get() + key=|route: &AppRoute| route.key.to_string() + children=move |route: AppRoute| { + let path = route.path.to_string(); + let is_active = location.pathname.get().contains(&path); + view! { + <li class="mt-1 w-full"> + <NavItem + href=route.path.to_string() + icon=route.icon.to_string() + text=route.label.to_string() + is_active=is_active + /> + </li> + } + } + /> + + </ul> + </div> + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/components/stat/mod.rs b/crates/frontend/src/components/stat/mod.rs new file mode 100644 index 000000000..4f22af63a --- /dev/null +++ b/crates/frontend/src/components/stat/mod.rs @@ -0,0 +1 @@ +pub mod stat; diff --git a/crates/frontend/src/components/stat/stat.rs b/crates/frontend/src/components/stat/stat.rs new file mode 100644 index 000000000..5fabe3f7f --- /dev/null +++ b/crates/frontend/src/components/stat/stat.rs @@ -0,0 +1,17 @@ +use leptos::*; + +#[component] +pub fn stat(heading: &'static str, icon: &'static str, number: String) -> impl IntoView { + let icon_class = format!("{} text-5xl", icon); + view! { + <div class="stats shadow"> + <div class="stat"> + <div class="stat-figure text-purple-700"> + <i class=icon_class></i> + </div> + <div class="stat-title">{heading}</div> + <div class="stat-value">{number}</div> + </div> + </div> + } +} diff --git a/crates/frontend/src/components/table/mod.rs b/crates/frontend/src/components/table/mod.rs new file mode 100644 index 000000000..604f33274 --- /dev/null +++ b/crates/frontend/src/components/table/mod.rs @@ -0,0 +1,2 @@ +pub mod table; +pub mod types; diff --git a/crates/frontend/src/components/table/table.rs b/crates/frontend/src/components/table/table.rs new file mode 100644 index 000000000..d6143e215 --- /dev/null +++ b/crates/frontend/src/components/table/table.rs @@ -0,0 +1,86 @@ +use super::types::Column; +use leptos::*; +use serde_json::{json, Map, Value}; + +fn generate_table_row_str(row: &Value) -> String { + match row { + Value::Null => "null".to_string(), + Value::String(rstr) => rstr.to_string(), + Value::Number(rnum) => rnum.to_string(), + Value::Bool(rbool) => rbool.to_string(), + Value::Array(rarr) => rarr + .iter() + .map(|ele| generate_table_row_str(ele)) + .collect::<Vec<String>>() + .join(","), + Value::Object(robj) => json!(robj).to_string(), + } +} + +#[component] +pub fn table( + key_column: String, + cell_style: String, + columns: Vec<Column>, + rows: Vec<Map<String, Value>>, +) -> impl IntoView { + view! { + <div class="overflow-x-auto"> + <table class="table table-zebra"> + <thead> + <tr> + <th></th> + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + view! { + <th class="uppercase">{&column.name.replace("_", " ")}</th> + } + }) + .collect_view()} + + </tr> + </thead> + <tbody> + + {rows + .iter() + .enumerate() + .map(|(index, row)| { + let row_id = row + .get(&key_column) + .unwrap_or(&json!("")) + .as_str() + .unwrap() + .to_string(); + view! { + <tr id=row_id> + <th>{index + 1}</th> + + {columns + .iter() + .filter(|column| !column.hidden) + .map(|column| { + let cname = &column.name; + let value: String = generate_table_row_str( + row.get(cname).unwrap_or(&Value::String("".to_string())), + ); + view! { + <td class=cell_style + .to_string()>{(column.formatter)(&value, &row)}</td> + } + }) + .collect_view()} + + </tr> + } + }) + .collect_view()} + + </tbody> + </table> + </div> + } +} diff --git a/crates/frontend/src/components/table/types.rs b/crates/frontend/src/components/table/types.rs new file mode 100644 index 000000000..a1dfbea76 --- /dev/null +++ b/crates/frontend/src/components/table/types.rs @@ -0,0 +1,48 @@ +use std::rc::Rc; + +use leptos::{view, IntoView, View}; +use serde_json::{Map, Value}; + +pub type CellFormatter = Box<Rc<dyn Fn(&str, &Map<String, Value>) -> View>>; + +#[derive(Clone, Debug)] +pub struct TableSettings { + pub redirect_prefix: Option<String>, +} + +#[derive(Clone)] +pub struct Column { + pub name: String, + pub hidden: bool, + pub formatter: CellFormatter, +} + +impl PartialEq for Column { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.hidden == other.hidden + } +} + +fn default_formatter(value: &str, _row: &Map<String, Value>) -> View { + view! { <span>{value.to_string()}</span> }.into_view() +} + +impl Column { + pub fn default(name: String) -> Column { + Column { + name, + hidden: false, + formatter: Box::new(Rc::new(default_formatter)), + } + } + pub fn new<NF>(name: String, hidden: Option<bool>, formatter: NF) -> Column + where + NF: Fn(&str, &Map<String, Value>) -> View + 'static, + { + Column { + name, + hidden: hidden.unwrap_or(false), + formatter: Box::new(Rc::new(formatter)), + } + } +} diff --git a/crates/frontend/src/components/variant_form/mod.rs b/crates/frontend/src/components/variant_form/mod.rs new file mode 100644 index 000000000..9170f7f28 --- /dev/null +++ b/crates/frontend/src/components/variant_form/mod.rs @@ -0,0 +1 @@ +pub mod variant_form; diff --git a/crates/frontend/src/components/variant_form/variant_form.rs b/crates/frontend/src/components/variant_form/variant_form.rs new file mode 100644 index 000000000..42ae91b93 --- /dev/null +++ b/crates/frontend/src/components/variant_form/variant_form.rs @@ -0,0 +1,290 @@ +use std::collections::HashSet; + +use chrono::Local; +use leptos::*; +use serde_json::{json, Map, Value}; + +use crate::{ + components::{ + dropdown::dropdown::{Dropdown, DropdownBtnType, DropdownDirection}, + override_form::override_form::OverrideForm, + }, + types::{DefaultConfig, Variant, VariantType}, +}; + +fn get_override_keys_from_variants(variants: &[(String, Variant)]) -> HashSet<String> { + variants + .iter() + .map(|(_, variant)| { + variant + .overrides + .keys() + .map(String::from) + .collect::<Vec<String>>() + }) + .flatten() + .collect::<HashSet<String>>() +} + +fn get_init_state(variants: &[(String, Variant)]) -> HashSet<String> { + let init_override_keys = get_override_keys_from_variants(variants); + + init_override_keys +} + +#[component] +pub fn variant_form<HC>( + edit: bool, + variants: Vec<(String, Variant)>, + default_config: Vec<DefaultConfig>, + handle_change: HC, +) -> impl IntoView +where + HC: Fn(Vec<(String, Variant)>) + 'static + Clone, +{ + let init_override_keys = get_init_state(&variants); + let (f_variants, set_variants) = create_signal(variants); + let (override_keys, set_override_keys) = create_signal(init_override_keys); + + let default_config = StoredValue::new(default_config); + let handle_change = StoredValue::new(handle_change); + let unused_config_keys = Signal::derive(move || { + default_config + .get_value() + .into_iter() + .filter(|config| !override_keys.get().contains(&config.key)) + .collect::<Vec<DefaultConfig>>() + }); + + let handle_control_override_key_remove = move |removed_key: String| { + logging::log!("Removing key {:?}", removed_key); + + set_override_keys.update(|current_override_keys| { + current_override_keys.remove(&removed_key); + }); + set_variants.update(|current_variants| { + for variant in current_variants.iter_mut() { + variant.1.overrides.remove(&removed_key); + } + }); + }; + + let handle_override_form_change = move |variant_idx: usize| { + let callback = move |updated_overrides: Map<String, Value>| { + set_variants.update_untracked(|curr_variants| { + curr_variants[variant_idx].1.overrides = updated_overrides.clone(); + handle_change.get_value()(curr_variants.clone()); + }); + }; + callback + }; + + create_effect(move |_| { + let f_variants = f_variants.get(); + handle_change.get_value()(f_variants.clone()); + }); + + let handle_config_key_select = move |default_config: DefaultConfig| { + let config_key = default_config.key; + set_variants.update(|current_variants: &mut Vec<(String, Variant)>| { + for (_, variant) in current_variants.iter_mut() { + variant.overrides.insert(config_key.clone(), json!("")); + } + }); + set_override_keys.update(|value: &mut HashSet<String>| { + value.insert(config_key.clone()); + }); + }; + + view! { + <div class="form-control w-full"> + <div class="flex items-center justify-between gap-4"> + <label class="label flex-col justify-center items-start"> + <span class="label-text font-semibold text-base">Experiment Variants</span> + <span class="label-text text-slate-400"> + "These are the override sets that would apply on the above context" + </span> + </label> + + </div> + <For + each=move || { + f_variants + .get() + .into_iter() + .enumerate() + .collect::<Vec<(usize, (String, Variant))>>() + } + + key=|(_, (key, _))| key.to_string() + children=move |(idx, (_, variant))| { + let is_control_variant = variant.variant_type == VariantType::CONTROL; + let handle_change = handle_override_form_change(idx); + let variant_type_label = match variant.variant_type { + VariantType::CONTROL => "Control".to_string(), + VariantType::EXPERIMENTAL => format!("Variant {idx}"), + }; + view! { + <div class="my-2 p-4 rounded bg-gray-50"> + <div class="flex items-center justify-between"> + <label class="label"> + <span class="label-text font-semibold text-base"> + {variant_type_label} + </span> + </label> + <Show when=move || { + is_control_variant && override_keys.get().len() > 0 + }> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + </Show> + </div> + <div class="flex items-center gap-4 my-4"> + <div class="form-control"> + <label class="label"> + <span class="label-text">ID</span> + </label> + </div> + <div class="form-control w-2/5"> + <input + name="variantId" + value=move || variant.id.to_string() + disabled=edit + type="text" + placeholder="Type a unique name here" + class="input input-bordered w-full max-w-xs h-10" + on:input=move |event| { + let variant_id = event_target_value(&event); + set_variants + .update(|current_variants: &mut Vec<(String, Variant)>| { + let variant_to_be_updated = current_variants.get_mut(idx); + match variant_to_be_updated { + Some((_, ref mut variant)) => { + variant.id = variant_id; + } + None => { + logging::log!( + "variant not found to update with id: {:?}", variant_id + ) + } + } + }); + } + /> + + </div> + </div> + <div class="mt-2"> + <Show when=move || { + is_control_variant && override_keys.get().len() == 0 + }> + <div class="my-4 flex flex-col justify-between items-center"> + <Dropdown + dropdown_btn_type=DropdownBtnType::Link + dropdown_direction=DropdownDirection::Left + dropdown_text=String::from("Add Override") + dropdown_icon=String::from("ri-add-line") + dropdown_options=unused_config_keys.get() + on_select=Box::new(handle_config_key_select) + /> + <div> + <span class="label-text text-slate-400 text-sm"> + "Add keys from your config that you want to override in this experiment" + </span> + </div> + </div> + </Show> + + <Show when=move || { + !is_control_variant && override_keys.get().len() == 0 + }> + <div class="my-4 flex flex-col justify-between items-center"> + <span class="label-text text-slate-400 text-sm"> + "Keys added in CONTROL will appear here as well for override" + </span> + </div> + </Show> + + <Show when=move || { + override_keys.get().len() > 0 + }> + {move || { + let variant = f_variants.get().get(idx).unwrap().clone(); + let overrides = variant.1.overrides; + if is_control_variant { + view! { + <OverrideForm + overrides=overrides + default_config=default_config.get_value() + handle_change=handle_change + is_standalone=false + show_add_override=false + handle_key_remove=Some( + Callback::new(handle_control_override_key_remove), + ) + /> + } + } else { + view! { + <OverrideForm + overrides=overrides + default_config=default_config.get_value() + handle_change=handle_change + is_standalone=false + show_add_override=false + disable_remove=true + /> + } + } + }} + + </Show> + + </div> + </div> + } + } + /> + + <div> + <button + class="btn btn-purple-outline btn-sm text-xs m-1" + disabled=edit + on:click:undelegated=move |_| { + leptos::logging::log!("add new variant"); + set_variants + .update(|curr_variants| { + let total_variants = curr_variants.len(); + let key = Local::now().timestamp().to_string(); + let overrides = Map::from_iter( + override_keys.get().into_iter().map(|key| { (key, json!("")) }), + ); + curr_variants + .push(( + key, + Variant { + id: format!("variant-{}", total_variants), + variant_type: VariantType::EXPERIMENTAL, + context_id: None, + override_id: None, + overrides: overrides, + }, + )) + }); + } + > + + <i class="ri-add-line"></i> + Add Variant + </button> + </div> + + </div> + } +} diff --git a/crates/frontend/src/hoc/layout/layout.rs b/crates/frontend/src/hoc/layout/layout.rs new file mode 100644 index 000000000..a86874b9b --- /dev/null +++ b/crates/frontend/src/hoc/layout/layout.rs @@ -0,0 +1,41 @@ +use crate::components::side_nav::side_nav::SideNav; +use leptos::*; +use leptos_router::*; + +pub fn use_tenant() -> String { + let params_map = use_params_map(); + let route_context = use_route(); + logging::log!("use_route-params_map {:?}", params_map.get()); + logging::log!( + "use_route-original_path {:?}", + route_context.original_path() + ); + logging::log!("use_route-path {:?}", route_context.path()); + + match params_map.get().get("tenant") { + Some(tenant) => tenant.clone(), + None => String::from("no-tenant"), + } +} + +#[component] +pub fn Layout(children: Children) -> impl IntoView { + let (tenant_rs, tenant_ws) = create_signal(use_tenant()); + provide_context(tenant_rs); + provide_context(tenant_ws); + + let route_context = use_route(); + let original_path = route_context.original_path(); + let path = route_context.path(); + // let params_map = route_context.params(); + + view! { + <div> + <SideNav resolved_path=path original_path=original_path.to_string()/> + // params_map=params_map + <main class="ease-soft-in-out xl:ml-96 relative h-full max-h-screen rounded-xl transition-all duration-200 overflow-y-auto"> + {children()} + </main> + </div> + } +} diff --git a/crates/frontend/src/hoc/layout/mod.rs b/crates/frontend/src/hoc/layout/mod.rs new file mode 100644 index 000000000..dd6461994 --- /dev/null +++ b/crates/frontend/src/hoc/layout/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/crates/frontend/src/hoc/mod.rs b/crates/frontend/src/hoc/mod.rs new file mode 100644 index 000000000..dd6461994 --- /dev/null +++ b/crates/frontend/src/hoc/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs new file mode 100644 index 000000000..2316a0b2f --- /dev/null +++ b/crates/frontend/src/lib.rs @@ -0,0 +1,28 @@ +mod api; +pub mod app; +pub mod components; +pub mod hoc; +pub mod pages; +pub mod types; +mod utils; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "hydrate")] { + use wasm_bindgen::prelude::wasm_bindgen; + + #[wasm_bindgen] + pub fn hydrate() { + use app::*; + use leptos::*; + + + console_error_panic_hook::set_once(); + let envs = utils::use_env(); + + leptos::mount_to_body(move || { + view! { <App app_envs={envs.clone()} /> } + }); + } + } +} diff --git a/crates/frontend/src/pages/ContextOverride/context_override.rs b/crates/frontend/src/pages/ContextOverride/context_override.rs new file mode 100644 index 000000000..fd17fa020 --- /dev/null +++ b/crates/frontend/src/pages/ContextOverride/context_override.rs @@ -0,0 +1,343 @@ +use std::rc::Rc; + +use crate::api::fetch_config; +use crate::api::{fetch_default_config, fetch_dimensions}; +use crate::components::button::button::Button; +use crate::components::condition_pills::condition_pills::ContextPills; +use crate::components::context_form::context_form::ContextForm; +use crate::components::context_form::utils::create_context; +use crate::components::override_form::override_form::OverrideForm; +use crate::components::table::{table::Table, types::Column}; +use crate::types::Dimension; +use crate::utils::modal_action; +use leptos::*; +use serde_json::{Map, Value}; +use web_sys::MouseEvent; + +#[component] +fn context_modal_form<NF>( + handle_change: NF, + dimensions: Resource<String, Result<Vec<Dimension>, ServerFnError>>, +) -> impl IntoView +where + NF: Fn(Vec<(String, String, String)>) + 'static + Clone, +{ + view! { + <div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + { + let handle_change_clone = handle_change.clone(); + move || { + let handle_change_clone_clone = handle_change_clone.clone(); + dimensions + .with(move |result| { + match result { + Some(Ok(dimension)) => { + view! { + <div> + <ContextForm + dimensions=dimension.clone() + context=vec![] + is_standalone=false + handle_change=handle_change_clone_clone.clone() + /> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + } + } + None => { + view! { <div>Loading....</div> } + } + } + }) + } + } + + </Suspense> + </div> + } +} + +#[component] +fn override_modal_form<NF>(handle_change: NF) -> impl IntoView +where + NF: Fn(Map<String, Value>) + 'static + Clone, +{ + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let default_config = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_default_config(current_tenant.clone()), + ); + + view! { + <div> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + { + let handle_change_clone = handle_change.clone(); + move || { + let handle_change_clone_clone = handle_change_clone.clone(); + default_config + .with(move |result| { + match result { + Some(Ok(config)) => { + view! { + <div> + <OverrideForm + overrides=Map::new() + default_config=config.clone() + is_standalone=false + handle_change=handle_change_clone_clone.clone() + /> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + } + } + None => { + view! { <div>Loading....</div> } + } + } + }) + } + } + + </Suspense> + </div> + } +} + +#[component] +fn modal_component(handle_submit: Rc<dyn Fn()>) -> impl IntoView { + let (context_condition, set_context_condition) = + create_signal::<Vec<(String, String, String)>>(vec![]); + let (overrides, set_overrides) = create_signal::<Map<String, Value>>(Map::new()); + + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let handle_context_change = move |updated_ctx: Vec<(String, String, String)>| { + set_context_condition.set(updated_ctx); + }; + let handle_overrides_change = move |updated_overrides: Map<String, Value>| { + set_overrides.set(updated_overrides); + }; + + let dimensions = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_dimensions(current_tenant.clone()), + ); + + let (error_message, set_error_message) = create_signal("".to_string()); + + let on_submit = { + move |ev: MouseEvent| { + let handle_submit_clone = handle_submit.clone(); + let current_tenant = tenant_rs.get(); + ev.prevent_default(); + + logging::log!("tirggering submit"); + + spawn_local({ + let handle_submit = handle_submit_clone; + let overrides = move || overrides.get(); + let context_conditions = move || context_condition.get(); + let dimensions = move || dimensions.get(); + async move { + let result = create_context( + current_tenant, + overrides(), + context_conditions(), + dimensions().unwrap().expect("resource not loaded"), + ) + .await; + + match result { + Ok(_) => { + handle_submit(); + modal_action("my_modal_5", "close") + } + Err(e) => { + if e.is_empty() { + set_error_message + .set("Internal_Server_error".to_string()); + } else { + set_error_message.set(e); + } + } + } + } + }); + } + }; + + view! { + <dialog id="my_modal_5" class="modal"> + <div class="modal-box relative bg-white w-12/12 max-w-4xl"> + <form method="dialog" class="flex justify-end"> + <button> + <i class="ri-close-fill"></i> + </button> + </form> + <form class="form-control w-full mt-8 bg-white text-gray-700 font-mono"> + <div> + <ContextModalForm + handle_change=handle_context_change + dimensions=dimensions + /> + </div> + <div class="mt-7"> + <OverrideModalForm handle_change=handle_overrides_change/> + </div> + <div class="form-control mt-7"> + <Button + text="Submit".to_string() + on_click=move |ev: MouseEvent| on_submit(ev) + /> + </div> + <div class="mt-7"> + <p class="text-red-500">{move || error_message.get()}</p> + </div> + </form> + </div> + </dialog> + } +} + +#[component] +pub fn context_override() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + + let context_data: Vec<(String, String, String)> = vec![]; + let ctx: RwSignal<Vec<(String, String, String)>> = create_rw_signal(context_data); + + let override_data = Map::new(); + + let ovr_data = create_rw_signal(override_data); + + provide_context(ctx); + + provide_context(ovr_data); + + let config_data = create_blocking_resource( + move || tenant_rs.get(), + move |current_tenant| fetch_config(current_tenant.clone()), + ); + + let table_columns = create_memo(move |_| { + vec![ + Column::default("KEY".to_string()), + Column::default("VALUE".to_string()), + ] + }); + + view! { + <div class="p-8"> + <div class="flex justify-between"> + <h2 class="card-title">Overrides</h2> + <Button + text="Create Context Overrides".to_string() + on_click=|_| modal_action("my_modal_5", "open") + /> + </div> + <div class="space-y-6"> + <ModalComponent handle_submit=Rc::new(move || config_data.refetch())/> + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + {move || { + config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let mut contexts: Vec<Map<String, Value>> = Vec::new(); + let mut context_views = Vec::new(); + let mut override_signal = Map::new(); + for context in config.contexts.iter() { + for key in context.override_with_keys.iter() { + let mut map = Map::new(); + let ovr = config.overrides.get(key).unwrap(); + let ovr_obj = ovr.as_object().unwrap(); + for (key, value) in ovr_obj.iter() { + let trimmed_key = Value::String( + key.trim_matches('"').to_string(), + ); + let formatted_value = Value::String( + format!("{}", value).trim_matches('"').to_string(), + ); + override_signal + .insert(trimmed_key.to_string(), formatted_value.clone()); + map.insert("KEY".to_string(), trimmed_key); + map.insert("VALUE".to_string(), formatted_value); + contexts.push(map.clone()); + } + } + context_views + .push( + view! { + <div class="rounded-lg shadow bg-base-100 p-6 shadow"> + <div class="flex justify-between"> + <div class="flex items-center space-x-4"> + + <h3 class="card-title text-base timeline-box text-gray-800 bg-base-100 shadow-md font-mono"> + "Condition" + </h3> + <i class="ri-arrow-right-fill ri-xl text-blue-500"></i> + <ContextPills context=context.condition.clone()/> + </div> + <button class="p-2 rounded hover:bg-gray-200 transition-colors"> + <i class="ri-edit-line text-blue-500"></i> + </button> + </div> + <div class="space-x-4"> + <Table + cell_style="min-w-48 font-mono".to_string() + rows=contexts.clone() + key_column="id".to_string() + columns=table_columns.get() + /> + </div> + + </div> + }, + ); + contexts.clear(); + } + ovr_data.set(override_signal); + context_views + } + Some(Err(error)) => { + vec![ + view! { + <div class="text-red-500"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + }, + ] + } + None => vec![view! { <div>Loading....</div> }], + } + }) + }} + + </Suspense> + </div> + </div> + } +} diff --git a/crates/frontend/src/pages/ContextOverride/mod.rs b/crates/frontend/src/pages/ContextOverride/mod.rs new file mode 100644 index 000000000..a943f41f1 --- /dev/null +++ b/crates/frontend/src/pages/ContextOverride/mod.rs @@ -0,0 +1 @@ +pub mod context_override; diff --git a/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs new file mode 100644 index 000000000..ad2d1b28b --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/DefaultConfig.rs @@ -0,0 +1,214 @@ +use crate::api::fetch_default_config; +use crate::components::default_config_form::default_config_form::DefaultConfigForm; +use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; +use crate::components::stat::stat::Stat; +use crate::components::table::{table::Table, types::Column}; +use leptos::*; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; + +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub key: String, + pub value: String, + pub pattern: String, + pub type_: String, + pub function_name: Option<Value>, +} + +#[component] +pub fn DefaultConfig() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let default_config_resource = create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_default_config(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let selected_config = create_rw_signal::<Option<RowData>>(None); + + let table_columns = create_memo(move |_| { + let edit_col_formatter = move |_: &str, row: &Map<String, Value>| { + logging::log!("{:?}", row); + let row_key = row["key"].clone().to_string().replace("\"", ""); + let row_value = row["value"].clone().to_string().replace("\"", ""); + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) + .unwrap_or(HashMap::new()); + + let function_name = row["function_name"].clone().to_string(); + let fun_name = match function_name.as_str() { + "null" => None, + _ => Some(json!(function_name.replace("\"", ""))), + }; + + let pattern_or_enum = schema_object + .keys() + .find(|key| { + key.to_string() == "pattern".to_string() + || key.to_string() == "enum".to_string() + }) + .and_then(|val| Some(val.clone())) + .unwrap_or(String::new()); + + let row_type = match schema_object.get("type") { + Some(Value::String(type_)) if type_ == "string" => { + pattern_or_enum.clone() + } + Some(Value::String(type_)) if type_ == "number" => type_.clone(), + Some(Value::String(_)) => String::from("other"), + Some(_) | None => String::new(), + }; + + let row_pattern = match schema_object.get("type") { + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "pattern" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| Some(val.clone().to_string())) + .unwrap_or(String::new()) + .replace("\"", "") + } + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "enum" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| { + if let Value::Array(v) = val { + return format!( + "[{}]", + v.iter() + .map(|v| v.to_string()) + .collect::<Vec<String>>() + .join(",") + ) + .into(); + } + None + }) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) if type_ == "number" => String::new(), + Some(Value::String(_)) => schema, + _ => String::new(), + }; + + let edit_click_handler = move |_| { + let row_data = RowData { + key: row_key.clone(), + value: row_value.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + function_name: fun_name.clone(), + }; + logging::log!("{:?}", row_data); + selected_config.set(Some(row_data)); + open_drawer("default_config_drawer"); + }; + + let edit_icon: HtmlElement<html::I> = + view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; + + view! { <span class="cursor-pointer" on:click=edit_click_handler>{edit_icon}</span> }.into_view() + }; + vec![ + Column::default("key".to_string()), + Column::default("schema".to_string()), + Column::default("value".to_string()), + Column::default("function_name".to_string()), + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), + Column::new("EDIT".to_string(), None, edit_col_formatter), + ] + }); + + view! { + <div class="p-8"> + {move || { + let handle_close = move || { + close_drawer("default_config_drawer"); + selected_config.set(None); + }; + if let Some(selected_config_data) = selected_config.get() { + view! { + <Drawer id="default_config_drawer".to_string() header="Edit Key" handle_close=handle_close> + <DefaultConfigForm + edit=true + config_key=selected_config_data.key + config_value=selected_config_data.value + config_type=selected_config_data.type_ + config_pattern=selected_config_data.pattern + function_name=selected_config_data.function_name + handle_submit=move || { + default_config_resource.refetch(); + close_drawer("default_config_drawer"); + selected_config.set(None); + } + /> + </Drawer> + } + } else { + view! { + <Drawer id="default_config_drawer".to_string() header="Create New Key" handle_close=handle_close> + <DefaultConfigForm handle_submit=move || { + default_config_resource.refetch(); + close_drawer("default_config_drawer"); + }/> + </Drawer> + } + } + }} + + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + {move || { + let default_config = default_config_resource.get().unwrap_or(vec![]); + let total_default_config_keys = default_config.len().to_string(); + let table_rows = default_config + .into_iter() + .map(|config| { + let mut ele_map = json!(config).as_object().unwrap().to_owned(); + ele_map + .insert( + "created_at".to_string(), + json!(config.created_at.format("%v").to_string()), + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat heading="Config Keys" icon="ri-tools-line" number=total_default_config_keys/> + </div> + <div class="card rounded-lg w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Default Config" + </h2> + <DrawerBtn drawer_id="default_config_drawer".to_string()> + Create Key <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> + </div> + <Table + cell_style="min-w-48 font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + /> + </div> + </div> + } + }} + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/DefaultConfig/mod.rs b/crates/frontend/src/pages/DefaultConfig/mod.rs new file mode 100644 index 000000000..1a618fd7e --- /dev/null +++ b/crates/frontend/src/pages/DefaultConfig/mod.rs @@ -0,0 +1 @@ +pub mod DefaultConfig; diff --git a/crates/frontend/src/pages/Dimensions/Dimensions.rs b/crates/frontend/src/pages/Dimensions/Dimensions.rs new file mode 100644 index 000000000..1928d9e54 --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/Dimensions.rs @@ -0,0 +1,221 @@ +use crate::components::dimension_form::dimension_form::DimensionForm; +use crate::components::drawer::drawer::{close_drawer, open_drawer, Drawer, DrawerBtn}; +use crate::components::{ + stat::stat::Stat, + table::{table::Table, types::Column}, +}; +use leptos::*; +use serde_json::{json, Map, Value}; +use std::collections::HashMap; + +use crate::api::fetch_dimensions; + +#[derive(Clone, Debug, Default)] +pub struct RowData { + pub dimension: String, + pub priority: u16, + pub type_: String, + pub pattern: String, + pub function_name: Option<Value>, +} + +#[component] +pub fn Dimensions() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let dimensions_resource = create_blocking_resource( + move || tenant_rs.get(), + |current_tenant| async move { + match fetch_dimensions(current_tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let selected_dimension = create_rw_signal::<Option<RowData>>(None); + + let table_columns = create_memo(move |_| { + let edit_col_formatter = move |_: &str, row: &Map<String, Value>| { + logging::log!("Dimension row: {:?}", row); + let row_dimension = row["dimension"].clone().to_string().replace("\"", ""); + let row_priority_str = row["priority"].clone().to_string().replace("\"", ""); + let row_priority = match row_priority_str.parse::<u16>() { + Ok(val) => val, + Err(_) => 0 as u16, + }; + + let schema = row["schema"].clone().to_string(); + let schema_object = serde_json::from_str::<HashMap<String, Value>>(&schema) + .unwrap_or(HashMap::new()); + + let function_name = row["function_name"].clone().to_string(); + let fun_name = match function_name.as_str() { + "null" => None, + _ => Some(json!(function_name.replace("\"", ""))), + }; + + let pattern_or_enum = schema_object + .keys() + .find(|key| { + key.to_string() == "pattern".to_string() + || key.to_string() == "enum".to_string() + }) + .and_then(|val| Some(val.clone())) + .unwrap_or(String::new()); + + let row_type = match schema_object.get("type") { + Some(Value::String(type_)) if type_ == "string" => { + pattern_or_enum.clone() + } + Some(Value::String(type_)) if type_ == "number" => type_.clone(), + Some(Value::String(_)) => String::from("other"), + Some(_) | None => String::new(), + }; + + let row_pattern = match schema_object.get("type") { + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "pattern" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| Some(val.clone().to_string())) + .unwrap_or(String::new()) + .replace("\"", "") + } + Some(Value::String(type_)) + if type_ == "string" && pattern_or_enum == "enum" => + { + schema_object + .get(&pattern_or_enum) + .and_then(|val| { + if let Value::Array(v) = val { + return format!( + "[{}]", + v.iter() + .map(|v| v.to_string()) + .collect::<Vec<String>>() + .join(",") + ) + .into(); + } + None + }) + .unwrap_or(String::new()) + } + Some(Value::String(type_)) if type_ == "number" => String::new(), + Some(Value::String(_)) => schema, + _ => String::new(), + }; + + let edit_click_handler = move |_| { + let row_data = RowData { + dimension: row_dimension.clone(), + priority: row_priority.clone(), + type_: row_type.clone(), + pattern: row_pattern.clone(), + function_name: fun_name.clone(), + }; + logging::log!("{:?}", row_data); + selected_dimension.set(Some(row_data)); + open_drawer("dimension_drawer"); + }; + + let edit_icon: HtmlElement<html::I> = + view! { <i class="ri-pencil-line ri-xl text-blue-500"></i> }; + + view! { <span class="cursor-pointer" on:click=edit_click_handler>{edit_icon}</span> }.into_view() + }; + vec![ + Column::default("dimension".to_string()), + Column::default("priority".to_string()), + Column::default("schema".to_string()), + Column::default("function_name".to_string()), + Column::default("created_by".to_string()), + Column::default("created_at".to_string()), + Column::new("EDIT".to_string(), None, edit_col_formatter), + ] + }); + + view! { + <div class="p-8"> + {move || { + let handle_close = move || { + close_drawer("dimension_drawer"); + selected_dimension.set(None); + }; + if let Some(selected_dimension_data) = selected_dimension.get() { + view! { + <Drawer id="dimension_drawer".to_string() header="Edit Dimension" handle_close=handle_close> + <DimensionForm + edit=true + priority=selected_dimension_data.priority + dimension_name=selected_dimension_data.dimension + dimension_type=selected_dimension_data.type_ + dimension_pattern=selected_dimension_data.pattern + function_name=selected_dimension_data.function_name + handle_submit=move || { + dimensions_resource.refetch(); + selected_dimension.set(None); + close_drawer("dimension_drawer"); + } + /> + </Drawer> + } + } else { + view! { + <Drawer id="dimension_drawer".to_string() header="Create New Dimension" handle_close=handle_close> + <DimensionForm handle_submit=move || { + dimensions_resource.refetch(); + close_drawer("dimension_drawer"); + }/> + </Drawer> + } + } + }} + + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)...."</p> } + }> + {move || { + let value = dimensions_resource.get().unwrap_or(vec![]); + let total_items = value.len().to_string(); + let table_rows = value + .iter() + .map(|ele| { + let mut ele_map = json!(ele).as_object().unwrap().clone(); + ele_map + .insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()), + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>(); + view! { + <div class="pb-4"> + <Stat heading="Dimensions" icon="ri-ruler-2-fill" number=total_items/> + </div> + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title chat-bubble text-gray-800 dark:text-white bg-white font-mono"> + "Dimensions" + </h2> + <DrawerBtn drawer_id="dimension_drawer".to_string()> + Create Dimension <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> + </div> + <Table + cell_style="min-w-48 font-mono".to_string() + rows=table_rows + key_column="id".to_string() + columns=table_columns.get() + /> + </div> + </div> + } + }} + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/Dimensions/mod.rs b/crates/frontend/src/pages/Dimensions/mod.rs new file mode 100644 index 000000000..c39486264 --- /dev/null +++ b/crates/frontend/src/pages/Dimensions/mod.rs @@ -0,0 +1 @@ +pub mod Dimensions; diff --git a/crates/frontend/src/pages/Experiment/mod.rs b/crates/frontend/src/pages/Experiment/mod.rs new file mode 100644 index 000000000..db396ebb8 --- /dev/null +++ b/crates/frontend/src/pages/Experiment/mod.rs @@ -0,0 +1,144 @@ +use futures::join; +use leptos::*; +use leptos_router::use_params_map; +use serde::{Deserialize, Serialize}; + +use crate::{ + api::{fetch_default_config, fetch_dimensions, fetch_experiment}, + components::{ + experiment::experiment::Experiment, + experiment_conclude_form::experiment_conclude_form::ExperimentConcludeForm, + experiment_form::experiment_form::ExperimentForm, + experiment_ramp_form::utils::ramp_experiment, modal::modal::Modal, + }, + types::{DefaultConfig, Dimension, Experiment}, + utils::{close_modal, extract_conditions, show_modal}, +}; + +use crate::components::experiment_ramp_form::experiment_ramp_form::ExperimentRampForm; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + experiment: Option<Experiment>, + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, +} + +#[component] +pub fn experiment_page() -> impl IntoView { + let exp_params = use_params_map(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let source = move || { + let t = tenant_rs.get(); + let exp_id = + exp_params.with(|params| params.get("id").cloned().unwrap_or("1".into())); + (exp_id, t) + }; + + let combined_resource: Resource<(String, String), CombinedResource> = + create_blocking_resource(source, |(exp_id, tenant)| async move { + // Perform all fetch operations concurrently + let experiments_future = + fetch_experiment(exp_id.to_string(), tenant.to_string()); + let dimensions_future = fetch_dimensions(tenant.to_string()); + let config_future = fetch_default_config(tenant.to_string()); + + let (experiments_result, dimensions_result, config_result) = + join!(experiments_future, dimensions_future, config_future); + + // Construct the combined result, handling errors as needed + CombinedResource { + experiment: experiments_result.ok(), + dimensions: dimensions_result.unwrap_or_else(|_| vec![]), + default_config: config_result.unwrap_or_else(|_| vec![]), + } + }); + + let handle_start = move |experiment_id: String| { + spawn_local(async move { + let tenant = tenant_rs.get(); + let _ = ramp_experiment(&experiment_id, 0, &tenant).await; + combined_resource.refetch(); + }) + }; + + let handle_ramp = move || show_modal("ramp_form_modal"); + let handle_conclude = move || show_modal("conclude_form_modal"); + let handle_edit = move || show_modal("experiment_edit_form_modal"); + + view! { + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> + {move || { + let resource = match combined_resource.get() { + Some(res) => res, + None => return view! { <h1>Error fetching experiment</h1> }.into_view(), + }; + let experiment = resource.experiment; + let default_config = resource.default_config; + let dimensions = resource.dimensions; + match experiment { + Some(experiment) => { + let experiment_rf = experiment.clone(); + let experiment_cf = experiment.clone(); + let experiment_ef = experiment.clone(); + view! { + <Experiment + experiment=experiment.clone() + handle_start=handle_start + handle_ramp=handle_ramp + handle_conclude=handle_conclude + handle_edit=handle_edit + /> + <Modal + id="ramp_form_modal".to_string() + handle_close=move || { close_modal("ramp_form_modal") } + > + + <ExperimentRampForm + experiment=experiment_rf + handle_submit=move || { combined_resource.refetch() } + /> + + </Modal> + <Modal + id="conclude_form_modal".to_string() + handle_close=move || { close_modal("conclude_form_modal") } + > + + <ExperimentConcludeForm + experiment=experiment_cf + handle_submit=move || { combined_resource.refetch() } + /> + + </Modal> + <Modal + id="experiment_edit_form_modal".to_string() + classnames="w-12/12 max-w-5xl".to_string() + handle_close=move || { close_modal("experiment_edit_form_modal") } + > + + <ExperimentForm + edit=true + id=experiment.id + name=experiment_ef.name + context=extract_conditions(&experiment_ef.context) + .unwrap_or(vec![]) + variants=experiment_ef.variants + default_config=default_config + dimensions=dimensions + handle_submit=move || { combined_resource.refetch() } + /> + + </Modal> + } + .into_view() + } + None => view! { <h1>Error fetching experiment</h1> }.into_view(), + } + }} + + </Transition> + } +} diff --git a/crates/frontend/src/pages/Home/Home.rs b/crates/frontend/src/pages/Home/Home.rs new file mode 100644 index 000000000..a7275339b --- /dev/null +++ b/crates/frontend/src/pages/Home/Home.rs @@ -0,0 +1,547 @@ +use std::time::Duration; + +use crate::components::condition_pills::condition_pills::ContextPills; +use crate::{ + api::{fetch_config, fetch_dimensions}, + components::{ + button::button::Button, context_form::context_form::ContextForm, + dropdown::dropdown::DropdownDirection, + }, + utils::{check_url_and_return_val, get_element_by_id, get_host}, +}; +use leptos::*; +use serde_json::{Map, Value}; +use strum::EnumProperty; +use strum_macros::Display; +use wasm_bindgen::JsCast; +use web_sys::{ + HtmlButtonElement, HtmlInputElement, HtmlSelectElement, HtmlSpanElement, MouseEvent, +}; + +#[derive(Clone, Debug, Copy, Display, strum_macros::EnumProperty, PartialEq)] +enum ResolveTab { + #[strum(props(id = "resolved_config_tab"))] + ResolvedConfig, + // #[strum(props(id = "selected_configs_tab"))] + // SelectedConfig, + #[strum(props(id = "all_configs_tab"))] + AllConfig, +} + +async fn resolve_config(tenant: String, context: String) -> Result<Value, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/config/resolve?{context}"); + match client + .get(url) + .query(&[("show_reasoning", "true")]) + .header("x-tenant", tenant) + .send() + .await + { + Ok(response) => { + let config = response.json().await.map_err(|e| e.to_string())?; + Ok(config) + } + Err(e) => Err(e.to_string()), + } +} + +fn gen_name_id(s0: &String, s1: &String, s2: &String) -> String { + format!("{s0}::{s1}::{s2}") +} + +#[component] +pub fn home() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let config_data = create_blocking_resource( + move || tenant_rs.get(), + move |tenant| fetch_config(tenant), + ); + let dimension_resource = create_resource( + move || tenant_rs.get(), + |tenant| async { + match fetch_dimensions(tenant).await { + Ok(data) => data, + Err(_) => vec![], + } + }, + ); + + let (selected_tab_rs, selected_tab_ws) = create_signal(ResolveTab::AllConfig); + + let unstrike = |search_field_prefix: &String, config: &Map<String, Value>| { + for (dimension, value) in config.into_iter() { + let search_field_prefix = if search_field_prefix.is_empty() { + dimension + } else { + &search_field_prefix + }; + let search_field_prefix = gen_name_id( + search_field_prefix, + dimension, + &value + .as_str() + .unwrap_or(&value.to_string().trim_matches('"')[..]) + .to_string(), + ); + logging::log!("search field prefix {:#?}", search_field_prefix); + let config_name_elements = document() + .get_elements_by_name(format!("{search_field_prefix}-1").as_str()); + let config_value_elements = document() + .get_elements_by_name(format!("{search_field_prefix}-2").as_str()); + logging::log!("config_name_elements {:#?}", config_name_elements.length()); + logging::log!( + "config_value_elements {:#?}", + config_value_elements.length() + ); + for i in 0..config_name_elements.length() { + let item_one = config_name_elements.item(i).expect("missing span"); + let item_two = config_value_elements.item(i).expect("missing span"); + + let (config_name_element, config_value_element) = ( + item_one.dyn_ref::<HtmlSpanElement>().unwrap(), + item_two.dyn_ref::<HtmlSpanElement>().unwrap(), + ); + let _ = config_name_element + .class_list() + .add_2("text-black", "font-bold"); + let _ = config_name_element + .class_list() + .remove_2("text-gray-300", "line-through"); + let _ = config_value_element + .class_list() + .add_2("text-black", "font-bold"); + let _ = config_value_element + .class_list() + .remove_2("text-gray-300", "line-through"); + logging::log!( + "config name after replace {} and value {}", + config_name_element.to_string(), + config_value_element.to_string() + ); + } + } + }; + + let gen_query_context = |query: Vec<(String, String, String)>| -> String { + let mut context: Vec<String> = vec![]; + for (dimension, op, value) in query.iter() { + let op = match op.as_str() { + "==" => "=", + _ => break, // query params do not support the other operators : != and IN, do something differently later + }; + context.push(format!("{}{op}{}", dimension, value.to_lowercase())); + } + context.join("&").to_string() + }; + + let resolve_click = move |ev: MouseEvent| { + ev.prevent_default(); + let dimension_labels = document().get_elements_by_name("context-dimension-name"); + let dimension_ops = document().get_elements_by_name("context-dimension-operator"); + let dimension_values = document().get_elements_by_name("context-dimension-value"); + let mut query_vector: Vec<(String, String, String)> = vec![]; + for i in 0..dimension_labels.length() { + query_vector.push(( + dimension_labels + .item(i) + .expect("missing input") + .dyn_ref::<HtmlInputElement>() + .unwrap() + .value(), + dimension_ops + .item(i) + .expect("missing input") + .dyn_ref::<HtmlSelectElement>() + .unwrap() + .value(), + dimension_values + .item(i) + .expect("missing input") + .dyn_ref::<HtmlInputElement>() + .unwrap() + .value(), + )) + } + // strike out all config elements on the page + let config_name_elements = document().get_elements_by_class_name("config-name"); + let config_value_elements = document().get_elements_by_class_name("config-value"); + for i in 0..config_name_elements.length() { + let (config_name_element, config_value_element) = ( + config_name_elements.item(i).unwrap(), + config_value_elements.item(i).unwrap(), + ); + let _ = config_name_element + .class_list() + .remove_2("text-black", "font-bold"); + let _ = config_name_element + .class_list() + .add_2("text-gray-300", "line-through"); + let _ = config_value_element + .class_list() + .remove_2("text-black", "font-bold"); + let _ = config_value_element + .class_list() + .add_2("text-gray-300", "line-through"); + } + logging::log!("query vector {:#?}", query_vector); + // resolve the context and get the config that would apply + spawn_local(async move { + let context = gen_query_context(query_vector); + let mut config = match resolve_config(tenant_rs.get(), context).await.unwrap() + { + Value::Object(m) => m, + _ => Map::new(), + }; + logging::log!("resolved config {:#?}", config); + // unstrike those that we want to show the user + // if metadata field is found, unstrike only that override + match config.remove("metadata") { + Some(Value::Array(metadata)) => { + if metadata.len() == 0 { + logging::log!("unstrike default config"); + unstrike(&String::new(), &config); + } + for applied in metadata.iter() { + logging::log!("applied config {:#?}", applied); + applied["override"] + .as_array() + .unwrap_or(&vec![]) + .iter() + .for_each(|override_id| { + logging::log!("unstrike {:#?}", override_id); + unstrike( + &override_id.as_str().unwrap().to_string(), + &config, + ) + }); + } + } + _ => { + logging::log!( + "no metadata recieved, default config is the config to be used" + ); + } + } + logging::log!("unstrike default config if needed"); + unstrike(&String::new(), &config); + + if selected_tab_rs.get_untracked() == ResolveTab::ResolvedConfig { + let resolution_card = document() + .get_element_by_id("resolved_table_body") + .expect("resolve table card not found"); + + let mut table_rows = String::new(); + for (key, value) in config.iter() { + table_rows.push_str( + format!( + "<tr><td>{key}</td><td style='word-break: break-word;'>{}</td></tr>", + check_url_and_return_val(value.as_str().unwrap().to_owned()) + ) + .as_str(), + ) + } + resolution_card.set_inner_html(&table_rows); + } + }); + }; + view! { + <div class="flex w-full flex-col flex-wrap mt-5 justify-evenly"> + <div class="card mr-5 ml-5 mt-6 h-4/5 shadow bg-base-100"> + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + {move || { + dimension_resource + .with(|dimension| { + view! { + <div class="card flex flex-row m-2 bg-base-100"> + <div class="card-body"> + <h2 class="card-title">Resolve Configs</h2> + + <ContextForm + dimensions=dimension.to_owned().unwrap_or(vec![]) + context=vec![] + heading_sub_text="Query your configs".to_string() + dropdown_direction=DropdownDirection::Right + is_standalone=false + resolve_mode=true + handle_change=|_| () + /> + <div class="card-actions mt-6 justify-end"> + <Button + id="resolve_btn".to_string() + text="Resolve".to_string() + on_click=resolve_click + /> + </div> + </div> + </div> + } + }) + }} + + </Suspense> + </div> + <div role="tablist" class="tabs m-6 w-30 self-start tabs-lifted tabs-md"> + <a + role="tab" + id=ResolveTab::AllConfig.get_str("id").expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + ResolveTab::AllConfig => { + "tab tab-active [--tab-border-color:#a651f5] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(ResolveTab::AllConfig); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + All Contexts + </a> + <a + role="tab" + id=ResolveTab::ResolvedConfig + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + ResolveTab::ResolvedConfig => { + "tab tab-active [--tab-border-color:orange] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(ResolveTab::ResolvedConfig); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + Resolved Configuration + </a> + </div> + {move || { + selected_tab_rs + .with(|tab| { + match tab { + ResolveTab::AllConfig => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + {config_data + .with(move |result| { + match result { + Some(Ok(config)) => { + let rows = |k: &String, v: &Value, striked: bool| { + let mut view_vector = vec![]; + let default_iter = vec![(k.clone(), v.clone())]; + for (key, value) in v + .as_object() + .unwrap_or(&Map::from_iter(default_iter)) + .iter() + { + let key = key.replace("\"", "").trim().to_string(); + let value = value + .as_str() + .unwrap_or(&value.to_string().trim_matches('"')[..]) + .into(); + let unique_name = gen_name_id(k, &key, &value); + view_vector + .push( + view! { + < tr > < td class = "min-w-48 font-mono" > < span name = + format!("{unique_name}-1") class = "config-name" class : + text - black = { ! striked } class : font - bold = { ! + striked } class : text - gray - 300 = { striked } > { key } + </ span > </ td > < td class = "min-w-48 font-mono" style = + "word-break: break-word;" > < span name = + format!("{unique_name}-2") class = "config-value" class : + text - black = { ! striked } class : font - bold = { ! + striked } class : text - gray - 300 = { striked } > { + check_url_and_return_val(value) } </ span > </ td > </ tr > + }, + ) + } + view_vector + }; + let contexts_views: Vec<_> = config + .contexts + .iter() + .map(|context| { + let rows: Vec<_> = context + .override_with_keys + .iter() + .filter_map(|key| { + let o = config.overrides.get(key); + if o.is_some() { Some((key, o.unwrap())) } else { None } + }) + .map(|(k, v)| { rows(&k, &v, true) }) + .collect(); + view! { + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title"> + <ContextPills context=context.condition.clone()/> + </h2> + <table class="table table-zebra mt-10"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{rows}</tbody> + </table> + + </div> + </div> + } + }) + .collect::<Vec<_>>(); + let new_context_views = contexts_views + .into_iter() + .rev() + .collect::<Vec<_>>(); + let default_config: Vec<_> = config + .default_configs + .iter() + .map(|(k, v)| { rows(&k, &v, false) }) + .collect(); + vec![ + view! { + <div class="mb-4 overflow-y-scroll"> + {new_context_views} + <div class="card bg-base-100 shadow m-6"> + <div class="card-body"> + <h2 class="card-title">Default Configuration</h2> + <table class="table table-zebra"> + <thead> + <tr> + <th>Key</th> + <th>Value</th> + </tr> + </thead> + <tbody>{default_config}</tbody> + </table> + </div> + </div> + </div> + }, + ] + } + Some(Err(error)) => { + vec![ + view! { + <div class="error"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + }, + ] + } + None => { + vec![ + view! { + <div class="error">{"No config data fetched"}</div> + }, + ] + } + } + })} + + </Suspense> + } + } + ResolveTab::ResolvedConfig => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + + {config_data + .with(move |conf| { + match conf { + Some(Ok(config)) => { + let default_configs = config.default_configs.clone(); + view! { + <div class="card m-6 shadow bg-base-100"> + <div class="card-body"> + <h2 class="card-title">Resolved Config</h2> + <table class="table table-zebra"> + <thead> + <tr> + <th>Config Key</th> + <th>Value</th> + </tr> + </thead> + <tbody id="resolved_table_body"> + <For + each=move || { default_configs.clone().into_iter() } + + key=|(key, value)| format!("{key}-{value}") + children=move |(config, value)| { + view! { + <tr> + <td>{config}</td> + <td style="word-break: break-word;"> + {match value { + Value::String(s) => check_url_and_return_val(s), + Value::Number(num) => num.to_string(), + Value::Bool(b) => b.to_string(), + _ => "".into(), + }} + + </td> + + </tr> + } + } + /> + + </tbody> + </table> + </div> + </div> + } + } + Some(Err(error)) => { + view! { + <div class="error"> + {"Failed to fetch config data: "} {error.to_string()} + </div> + } + } + None => { + view! { + <div class="error">{"No config data fetched"}</div> + } + } + } + })} + + </Suspense> + } + } + } + }) + }} + + </div> + } +} diff --git a/crates/frontend/src/pages/Home/mod.rs b/crates/frontend/src/pages/Home/mod.rs new file mode 100644 index 000000000..22b9ee779 --- /dev/null +++ b/crates/frontend/src/pages/Home/mod.rs @@ -0,0 +1,2 @@ +#![allow(non_snake_case)] +pub mod Home; diff --git a/crates/frontend/src/pages/NotFound/NotFound.rs b/crates/frontend/src/pages/NotFound/NotFound.rs new file mode 100644 index 000000000..af3170add --- /dev/null +++ b/crates/frontend/src/pages/NotFound/NotFound.rs @@ -0,0 +1,20 @@ +use leptos::*; + +#[component] +pub fn not_found() -> impl IntoView { + // set an HTTP status code 404 + // this is feature gated because it can only be done during + // initial server-side rendering + // if you navigate to the 404 page subsequently, the status + // code will not be set because there is not a new HTTP request + // to the server + //#[cfg(feature = "ssr")] + //{ + // this can be done inline because it's synchronous + // if it were async, we'd use a server function + // let resp = expect_context::<leptos_actix::ResponseOptions>(); + //resp.set_status(actix_web::http::StatusCode::NOT_FOUND); + //} + + view! { <h1>"Not Found"</h1> } +} diff --git a/crates/frontend/src/pages/NotFound/mod.rs b/crates/frontend/src/pages/NotFound/mod.rs new file mode 100644 index 000000000..e96bf6956 --- /dev/null +++ b/crates/frontend/src/pages/NotFound/mod.rs @@ -0,0 +1 @@ +pub mod NotFound; diff --git a/crates/frontend/src/pages/experiment_list/experiment_list.rs b/crates/frontend/src/pages/experiment_list/experiment_list.rs new file mode 100644 index 000000000..4af09fd6a --- /dev/null +++ b/crates/frontend/src/pages/experiment_list/experiment_list.rs @@ -0,0 +1,245 @@ +use futures::join; +use leptos::logging::*; +use leptos::*; + +use chrono::{prelude::Utc, TimeZone}; +use serde::{Deserialize, Serialize}; + +use crate::components::drawer::drawer::{close_drawer, Drawer, DrawerBtn}; +use crate::components::{ + experiment_form::experiment_form::ExperimentForm, pagination::pagination::Pagination, + stat::stat::Stat, table::table::Table, +}; + +use crate::types::{ExperimentsResponse, ListFilters}; + +use super::utils::experiment_table_columns; +use crate::{ + api::{fetch_default_config, fetch_dimensions, fetch_experiments}, + types::{DefaultConfig, Dimension}, +}; +use serde_json::{json, Map, Value}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + experiments: ExperimentsResponse, + dimensions: Vec<Dimension>, + default_config: Vec<DefaultConfig>, +} + +#[component] +pub fn experiment_list() -> impl IntoView { + // acquire tenant + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (filters, set_filters) = create_signal(ListFilters { + status: None, + from_date: Utc.timestamp_opt(0, 0).single(), + to_date: Utc.timestamp_opt(4130561031, 0).single(), + page: Some(1), + count: Some(10), + }); + + let (reset_exp_form, set_exp_form) = create_signal(0); + let table_columns = create_memo(move |_| experiment_table_columns()); + + let combined_resource: Resource<(String, ListFilters), CombinedResource> = + create_blocking_resource( + move || (tenant_rs.get(), filters.get()), + |(current_tenant, filters)| async move { + // Perform all fetch operations concurrently + let experiments_future = + fetch_experiments(filters, current_tenant.to_string()); + let dimensions_future = fetch_dimensions(current_tenant.to_string()); + let config_future = fetch_default_config(current_tenant.to_string()); + + let (experiments_result, dimensions_result, config_result) = + join!(experiments_future, dimensions_future, config_future); + + // Construct the combined result, handling errors as needed + CombinedResource { + experiments: experiments_result.unwrap_or_else(|_| { + ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + } + }), + dimensions: dimensions_result.unwrap_or_else(|_| vec![]), + default_config: config_result.unwrap_or_else(|_| vec![]), + } + }, + ); + + let handle_submit_experiment_form = move || { + combined_resource.refetch(); + set_exp_form.update(|val| { + *val += 1; + }); + close_drawer("create_exp_drawer"); + }; + + // TODO: Add filters + view! { + <div class="p-8"> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> + <div class="pb-4"> + + {move || { + let value = combined_resource.get(); + let total_items = match value { + Some(v) => v.experiments.total_items.to_string(), + _ => "0".to_string(), + }; + view! { + <Stat + heading="Experiments" + icon="ri-test-tube-fill" + number=total_items + /> + } + }} + + </div> + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title">Experiments</h2> + <div> + <DrawerBtn drawer_id="create_exp_drawer" + .to_string()> + Create Experiment <i class="ri-edit-2-line ml-2"></i> + </DrawerBtn> + </div> + </div> + <div> + + {move || { + let value = combined_resource.get(); + match value { + Some(v) => { + let data = v + .experiments + .data + .iter() + .map(|ele| { + let mut ele_map = json!(ele) + .as_object() + .unwrap() + .to_owned(); + ele_map + .insert( + "created_at".to_string(), + json!(ele.created_at.format("%v").to_string()), + ); + ele_map + .insert( + "last_modified".to_string(), + json!(ele.last_modified.format("%v").to_string()), + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + <Table + cell_style="min-w-48 font-mono".to_string() + rows=data + key_column="id".to_string() + columns=table_columns.get() + /> + } + } + None => view! { <div>Loading....</div> }.into_view(), + } + }} + + </div> + <div class="mt-2 flex justify-end"> + + {move || { + let current_page = filters.get().page.unwrap_or(0); + let total_pages = match combined_resource.get() { + Some(val) => val.experiments.total_pages, + None => 0, + }; + view! { + <Pagination + current_page=current_page + total_pages=total_pages + next=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p < total_pages => Some(p + 1), + Some(p) => Some(p), + None => None, + } + }); + } + + previous=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p > 1 => Some(p - 1), + Some(p) => Some(p), + None => None, + } + }); + } + /> + } + }} + + </div> + </div> + </div> + + {move || { + let dim = combined_resource + .get() + .unwrap_or(CombinedResource { + experiments: ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + }, + dimensions: vec![], + default_config: vec![], + }) + .dimensions; + let def_conf = combined_resource + .get() + .unwrap_or(CombinedResource { + experiments: ExperimentsResponse { + total_items: 0, + total_pages: 0, + data: vec![], + }, + dimensions: vec![], + default_config: vec![], + }) + .default_config; + let _ = reset_exp_form.get(); + view! { + <Drawer id="create_exp_drawer".to_string() header="Create New Experiment" handle_close=move || { + close_drawer("create_exp_drawer"); + }> + <ExperimentForm + name="".to_string() + context=vec![] + variants=vec![] + dimensions=dim.clone() + default_config=def_conf.clone() + handle_submit=handle_submit_experiment_form + /> + </Drawer> + } + }} + + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/experiment_list/mod.rs b/crates/frontend/src/pages/experiment_list/mod.rs new file mode 100644 index 000000000..5fdbbd12d --- /dev/null +++ b/crates/frontend/src/pages/experiment_list/mod.rs @@ -0,0 +1,2 @@ +pub mod experiment_list; +pub mod utils; diff --git a/crates/frontend/src/pages/experiment_list/utils.rs b/crates/frontend/src/pages/experiment_list/utils.rs new file mode 100644 index 000000000..e9a70f22d --- /dev/null +++ b/crates/frontend/src/pages/experiment_list/utils.rs @@ -0,0 +1,130 @@ +use crate::components::{ + condition_pills::condition_pills::ContextPills, table::types::Column, +}; +use core::time::Duration; +use leptos::*; +use leptos_router::A; +use serde_json::{json, Map, Value}; +use std::vec::Vec; +use web_sys::MouseEvent; + +pub fn experiment_table_columns() -> Vec<Column> { + vec![ + Column::new( + "name".to_string(), + None, + |value: &str, row: &Map<String, Value>| { + let (copied, set_copied) = create_signal(false); + + let experiment_name = value.to_string(); + let experiment_id = row.get("id").map_or(String::from(""), |value| { + value.as_str().unwrap_or("").to_string() + }); + let experiment_id_copy = experiment_id.clone(); + let handle_copy = move |event: MouseEvent| { + event.prevent_default(); + + let copy_code = format!( + "navigator.clipboard.writeText('{}')", + &experiment_id_copy + ); + match js_sys::eval(©_code) { + Ok(_) => { + set_copied.set(true); + set_timeout( + move || { + set_copied.set(false); + }, + Duration::new(1, 0), + ); + } + Err(_) => logging::log!("unable to copy to clipboard"), + } + }; + view! { + <div> + <A href=experiment_id.to_string() class="btn-link"> + {experiment_name} + </A> + <div class="text-gray-500"> + <span class="text-xs"> + {experiment_id} + </span> + <i class="ri-file-copy-line cursor-pointer ml-2" on:click:undelegated=handle_copy></i> + <Show when=move || copied.get()> + <div class="inline-block bg-gray-600 ml-2 rounded-xl px-2"> + <span class="text-white text-xs font-semibold"> + "copied!" + </span> + </div> + </Show> + </div> + </div> + } + .into_view() + }, + ), + Column::new( + "status".to_string(), + None, + |value: &str, row: &Map<String, Value>| { + let badge_color = match value { + "CREATED" => "badge-info", + "INPROGRESS" => "badge-warning", + "CONCLUDED" => "badge-success", + &_ => "info", + }; + let class = format!("badge {}", badge_color); + let traffic_percentage = row.get("traffic_percentage"); + let traffic_percentage = traffic_percentage + .map(|val| val.as_u64().unwrap_or(0)) + .unwrap_or(0); + view! { + <div class={class}> + <span class="text-white font-semibold text-xs"> + { + if value == "INPROGRESS" { + format!("{}: {}%", value.to_string(), traffic_percentage) + } else { + value.to_string() + } + } + </span> + </div> + } + .into_view() + }, + ), + Column::new( + "context".to_string(), + None, + |_, row: &Map<String, Value>| { + let context = match row.get("context") { + Some(value) => value.to_owned(), + None => json!(""), + }; + + view! { + <div class="inline-flex flex-col gap-y-2"> + <ContextPills context=context /> + </div> + } + .into_view() + }, + ), + Column::new("chosen_variant".to_string(), None, |value: &str, _| { + let label = match value { + "null" => "¯\\_(ツ)_/¯".to_string(), + other => other.to_string(), + }; + + view! { + <span>{label}</span> + } + .into_view() + }), + Column::default("created_at".to_string()), + Column::default("created_by".to_string()), + Column::default("last_modified".to_string()), + ] +} diff --git a/crates/frontend/src/pages/function/function.rs b/crates/frontend/src/pages/function/function.rs new file mode 100644 index 000000000..d1be9eec8 --- /dev/null +++ b/crates/frontend/src/pages/function/function.rs @@ -0,0 +1,505 @@ +use leptos::*; +use leptos_router::use_params_map; + +use crate::types::FunctionResponse; + +use super::utils::publish_function; + +use crate::api::fetch_function; +use std::time::Duration; +use strum::EnumProperty; +use strum_macros::Display; +use web_sys::MouseEvent; + +use crate::utils::get_element_by_id; +use web_sys::HtmlButtonElement; + +use crate::components::function_form::function_form::{FunctionEditor, TestForm}; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Copy, Display, strum_macros::EnumProperty, PartialEq)] +enum CodeTab { + #[strum(props(id = "draft_code_tab"))] + DraftCode, + #[strum(props(id = "published_code_tab"))] + PublishedCode, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + function: Option<FunctionResponse>, +} + +#[component] +pub fn function_page() -> impl IntoView { + let function_params = use_params_map(); + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let source = move || { + let t = tenant_rs.get(); + let function_name = function_params + .with(|params| params.get("function_name").cloned().unwrap_or("1".into())); + (function_name, t) + }; + + let (selected_tab_rs, selected_tab_ws) = create_signal(CodeTab::PublishedCode); + let (editor_mode_rs, editor_mode_ws) = create_signal(true); + let (test_mode_rs, test_mode_ws) = create_signal(false); + let (show_publish_rs, show_publish_ws) = create_signal(false); + let (publish_error_rs, publish_error_ws) = create_signal("".to_string()); + + let combined_resource: Resource<(String, String), CombinedResource> = + create_blocking_resource(source, |(function_name, tenant)| async move { + let function_result = + fetch_function(function_name.to_string(), tenant.to_string()).await; + + CombinedResource { + function: function_result.ok(), + } + }); + + view! { + <Transition fallback=move || { + view! { <h1>Loading....</h1> } + }> + {move || { + let resource = match combined_resource.get() { + Some(res) => res, + None => return view! { <h1>Error fetching function</h1> }.into_view(), + }; + let function = resource.function; + match function { + Some(function) => { + let function_ef = function.clone(); + let function_data = function.clone(); + publish_error_ws.set("".to_string()); + match function.published_at.clone() { + Some(val) => show_publish_ws.set(val < function.draft_edited_at), + None => show_publish_ws.set(true), + } + let publish_click = move |event: MouseEvent| { + event.prevent_default(); + logging::log!("Submitting function form"); + let tenant = tenant_rs.get(); + let f_function_name = function_ef.function_name.clone(); + spawn_local({ + async move { + let result = publish_function(f_function_name, tenant).await; + match result { + Ok(_) => { + publish_error_ws.set("".to_string()); + combined_resource.refetch(); + } + Err(e) => { + publish_error_ws.set(e); + } + } + } + }); + }; + view! { + + <div class="flex flex-col flex-row overflow-x-auto p-2 bg-transparent"> + + <div class="flex bg-base-100 flex-row gap-3 justify-between flex-wrap shadow m-5"> + <div class="stat w-2/12"> + <div class="stat-title">Function Name</div> + <div>{function.function_name.clone()}</div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Published Runtime Version</div> + <div> + {function + .published_runtime_version + .clone() + .unwrap_or("null".to_string())} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Function Description</div> + <div> + {format!("{}", function.function_description.clone())} + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Draft Edited At</div> + <div> + + {format!( + "{}", + function.draft_edited_at.clone().format("%v").to_string(), + )} + + </div> + </div> + <div class="stat w-2/12"> + <div class="stat-title">Published At</div> + <div> + + {match function.published_at.clone() { + Some(val) => val.format("%v").to_string(), + None => "null".to_string(), + }} + + </div> + </div> + + </div> + + <div class="flex flex-row justify-between"> + <div + role="tablist" + class="flex-row tabs m-6 w-30 self-start tabs-lifted tabs-md" + > + <a + role="tab" + id=CodeTab::PublishedCode + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + CodeTab::PublishedCode => { + "tab tab-active [--tab-border-color:#a651f5] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(CodeTab::PublishedCode); + publish_error_ws.set("".to_string()); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + Published Code + </a> + <a + role="tab" + id=CodeTab::DraftCode + .get_str("id") + .expect("ID not defined for Resolve tab") + class=move || match selected_tab_rs.get() { + CodeTab::DraftCode => { + "tab tab-active [--tab-border-color:orange] text-center" + } + _ => "tab", + } + + on:click=move |_| { + selected_tab_ws.set(CodeTab::DraftCode); + publish_error_ws.set("".to_string()); + set_timeout( + || { + get_element_by_id::<HtmlButtonElement>("resolve_btn") + .map(|btn| btn.click()); + }, + Duration::new(1, 0), + ); + } + > + + Draft Code + </a> + </div> + <div> + {move || { + selected_tab_rs + .with(|tab| { + match tab { + CodeTab::PublishedCode => { + view! { + <div> + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { test_mode_ws.set(false) } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + + </Show> + + <Show when=move || { test_mode_rs.get() == false }> + <div class="flex flex-row justify-end join m-5"> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { test_mode_ws.set(true) } + > + <i class="fontisto-test-tube-alt"></i> + Test + </button> + </div> + + </Show> + </div> + } + } + CodeTab::DraftCode => { + let publish_click_ef = publish_click.clone(); + view! { + <div> + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + test_mode_ws.set(false); + } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + </Show> + + <Show when=move || { editor_mode_rs.get() == false }> + <div class="flex flex-row justify-end join m-5"> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + editor_mode_ws.set(true); + } + > + <i class="gmdi-cancel-presentation-o"></i> + Cancel + + </button> + </div> + + </Show> + <div class="flex flex-row justify-end join m-5"> + <Show when=move || { + editor_mode_rs.get() == true && test_mode_rs.get() == false + && show_publish_rs.get() + }> + <div class="flex"> + <p class="text-red-500">{move || publish_error_rs.get()}</p> + </div> + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=publish_click_ef.clone() + > + <i class="fontisto-test-tube-alt"></i> + Publish + </button> + + </Show> + <Show when=move || { + editor_mode_rs.get() == true && test_mode_rs.get() == false + }> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + editor_mode_ws.set(false); + publish_error_ws.set("".to_string()) + } + > + <i class="ri-edit-line"></i> + Edit + + </button> + + <button + class="btn join-item text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 shadow-lgont-medium rounded-lg text-sm px-5 py-2.5 text-center" + on:click=move |_| { + test_mode_ws.set(true); + publish_error_ws.set("".to_string()) + } + > + <i class="fontisto-test-tube-alt"></i> + Test + </button> + + </Show> + </div> + </div> + } + } + } + }) + }} + + </div> + + </div> + + {move || { + let is_edit = editor_mode_rs.get(); + let is_test = test_mode_rs.get(); + let should_show = (editor_mode_rs.get() == true) + && (test_mode_rs.get() == false); + let fun_clone = function_data.clone(); + let fun_clone_ = function_data.clone(); + + let pub_code = fun_clone + .published_code + .clone() + .unwrap_or("//Code not published yet".to_string()); + selected_tab_rs + .with(|tab| { + match tab { + CodeTab::PublishedCode => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading (Suspense Fallback)..."</p> } + }> + + { + let fun_pub = fun_clone.clone(); + view! { + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + monaco.editor.create(document.querySelector('.monaco'), {{ + + value: `{pub_code}`, + language: 'javascript', + readOnly: true + }}); + "#, + )} + + </script> + + <Show when=move || { is_test == false }> + + <div class="monaco" style="min-height: 500px"></div> + + </Show> + + <Show when=move || { test_mode_rs.get() == true }> + <div class="flex-row"> + + <TestForm + function_name=fun_pub.function_name.clone() + stage="PUBLISHED".to_string() + handle_submit=move || {} + /> + + </div> + + </Show> + } + .into_view() + } + + </Suspense> + } + .into_view() + } + CodeTab::DraftCode => { + view! { + <Suspense fallback=move || { + view! { <p>"Loading..."</p> } + }> + + { + let function_edit = fun_clone_.clone(); + let function_test = fun_clone_.clone(); + let fun_code = fun_clone_.draft_code.clone(); + view! { + <Show when=move || { + editor_mode_rs.get() == false && test_mode_rs.get() == false + }> + <div class="flex-row"> + <FunctionEditor + edit=true + function_name=function_edit.function_name.clone() + function=function_edit.draft_code.clone() + runtime_version=function_edit.draft_runtime_version.clone() + description=function_edit.function_description.clone() + handle_submit=move || { + combined_resource.refetch(); + editor_mode_ws.set(true) + } + /> + </div> + </Show> + + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + window.editor = monaco.editor.create(document.querySelector('.monaco'), {{ + + value: `{fun_code}`, + language: 'javascript', + readOnly: {is_edit} + }}); + + const form = document.getElementById("MyForm"); + form.addEventListener("formdata", e => + {{ + e.formData.set('function', window.editor.getValue()); + }}); + + "#, + )} + + </script> + + <Show when=move || { should_show }> + + <div class="monaco" style="min-height: 500px"></div> + + </Show> + + <Show when=move || { + test_mode_rs.get() == true && editor_mode_rs.get() == true + }> + <div class="flex-row"> + + <TestForm + function_name=function_test.function_name.clone() + stage="DRAFT".to_string() + handle_submit=move || {} + /> + + </div> + + </Show> + } + .into_view() + } + + </Suspense> + } + } + } + }) + }} + + </div> + } + .into_view() + } + None => { + view! { + + <h1>Error fetching function</h1> + } + .into_view() + } + } + }} + + </Transition> + } +} diff --git a/crates/frontend/src/pages/function/function_create.rs b/crates/frontend/src/pages/function/function_create.rs new file mode 100644 index 000000000..937330cc9 --- /dev/null +++ b/crates/frontend/src/pages/function/function_create.rs @@ -0,0 +1,59 @@ +use crate::components::function_form::function_form::FunctionEditor; +use crate::types::FunctionResponse; +use leptos::*; +use leptos_router::use_navigate; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + function: Option<FunctionResponse>, +} + +#[component] +pub fn create_function_view() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + view! { + <div> + + <div class="mt-20 mb-20">Create Function</div> + + <script type="module"> + {format!( + r#" + + import * as monaco from 'https://cdn.jsdelivr.net/npm/monaco-editor@0.39.0/+esm'; + + window.editor = monaco.editor.create(document.querySelector('.monaco'), {{ + + value: [ + 'async function validate() {{', + '}}' + ].join('\n'), + language: 'javascript', + readOnly: false + }}); + + const form = document.getElementById("MyForm"); + form.addEventListener("formdata", e => + {{ + e.formData.set('function', window.editor.getValue()); + }}); + + "#, + )} + + </script> + <FunctionEditor + edit=false + handle_submit=move || { + let tenant = tenant_rs.get(); + let redirect_url = format!("admin/{tenant}/function"); + let navigate = use_navigate(); + navigate(redirect_url.as_str(), Default::default()) + } + /> + + </div> + } + .into_view() +} diff --git a/crates/frontend/src/pages/function/function_list.rs b/crates/frontend/src/pages/function/function_list.rs new file mode 100644 index 000000000..24ec320ea --- /dev/null +++ b/crates/frontend/src/pages/function/function_list.rs @@ -0,0 +1,174 @@ +use leptos::logging::*; +use leptos::*; + +use chrono::{prelude::Utc, TimeZone}; +use leptos_router::A; +use serde::{Deserialize, Serialize}; + +use crate::components::{ + pagination::pagination::Pagination, stat::stat::Stat, table::table::Table, +}; + +use crate::types::{FunctionResponse, ListFilters}; + +use super::utils::function_table_columns; +use crate::api::fetch_functions; +use serde_json::{json, Map, Value}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CombinedResource { + functions: Vec<FunctionResponse>, +} + +#[component] +pub fn function_list() -> impl IntoView { + let tenant_rs = use_context::<ReadSignal<String>>().unwrap(); + let (filters, set_filters) = create_signal(ListFilters { + status: None, + from_date: Utc.timestamp_opt(0, 0).single(), + to_date: Utc.timestamp_opt(4130561031, 0).single(), + page: Some(1), + count: Some(10), + }); + let table_columns = create_memo(move |_| function_table_columns()); + + let combined_resource: Resource<String, CombinedResource> = create_blocking_resource( + move || (tenant_rs.get()), + |current_tenant| async move { + let functions_future = fetch_functions(current_tenant.to_string()); + + let functions_result = functions_future.await; + CombinedResource { + functions: functions_result.unwrap_or_else(|_| vec![]), + } + }, + ); + + view! { + <div class="p-8"> + <Suspense fallback=move || view! { <p>"Loading (Suspense Fallback)..."</p> }> + <div class="pb-4"> + + {move || { + let value = combined_resource.get(); + let total_items = match value { + Some(v) => std::vec::Vec::len(&v.functions).to_string(), + _ => "0".to_string(), + }; + view! { + <Stat heading="Functions" icon="ri-code-box-fill" number=total_items/> + } + }} + + </div> + <div class="card rounded-xl w-full bg-base-100 shadow"> + <div class="card-body"> + <div class="flex justify-between"> + <h2 class="card-title">Functions</h2> + <div> + + <A + href="create".to_string() + class="btn-purple font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 btn-link" + > + <button> + Create Function <i class="ri-edit-2-line ml-2"></i> + </button> + </A> + </div> + </div> + <div> + + {move || { + let value = combined_resource.get(); + match value { + Some(v) => { + let data = v + .functions + .iter() + .map(|ele| { + let mut ele_map = json!(ele) + .as_object() + .unwrap() + .to_owned(); + ele_map + .insert( + "published_at".to_string(), + match ele.published_at { + Some(val) => json!(val.format("%v").to_string()), + None => json!("null".to_string()), + }, + ); + ele_map + }) + .collect::<Vec<Map<String, Value>>>() + .to_owned(); + view! { + + <Table + cell_style="".to_string() + rows=data + key_column="id".to_string() + columns=table_columns.get() + /> + } + } + None => { + view! { + + <div>Loading....</div> + } + .into_view() + } + } + }} + + </div> + <div class="mt-2 flex justify-end"> + + {move || { + let current_page = filters.get().page.unwrap_or(0); + let total_pages = match combined_resource.get() { + Some(val) => { + (val.functions.len() as f64 / 10 as f64).ceil() as i64 + } + None => 0, + }; + view! { + <Pagination + current_page=current_page + total_pages=total_pages + next=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p < total_pages => Some(p + 1), + Some(p) => Some(p), + None => None, + } + }); + } + + previous=move || { + set_filters + .update(|f| { + f + .page = match f.page { + Some(p) if p > 1 => Some(p - 1), + Some(p) => Some(p), + None => None, + } + }); + } + /> + } + }} + + </div> + </div> + </div> + </Suspense> + </div> + } +} diff --git a/crates/frontend/src/pages/function/mod.rs b/crates/frontend/src/pages/function/mod.rs new file mode 100644 index 000000000..8ae1f125d --- /dev/null +++ b/crates/frontend/src/pages/function/mod.rs @@ -0,0 +1,4 @@ +pub mod function; +pub mod function_create; +pub mod function_list; +pub mod utils; diff --git a/crates/frontend/src/pages/function/utils.rs b/crates/frontend/src/pages/function/utils.rs new file mode 100644 index 000000000..342c8f704 --- /dev/null +++ b/crates/frontend/src/pages/function/utils.rs @@ -0,0 +1,57 @@ +use crate::components::table::types::Column; +use crate::utils::get_host; +use leptos::*; +use leptos_router::A; +use reqwest::StatusCode; +use serde_json::{Map, Value}; +use std::vec::Vec; + +pub fn function_table_columns() -> Vec<Column> { + vec![ + Column::new( + "function_name".to_string(), + None, + |value: &str, _row: &Map<String, Value>| { + let function_name = value.to_string(); + view! { + <div> + <A href=function_name.clone() class="btn-link"> + {function_name} + </A> + + </div> + } + .into_view() + }, + ), + Column::default("function_description".to_string()), + Column::default("published_runtime_version".to_string()), + Column::default("draft_runtime_version".to_string()), + Column::default("published_at".to_string()), + Column::default("published_by".to_string()), + ] +} + +pub async fn publish_function( + function_name: String, + tenant: String, +) -> Result<String, String> { + let client = reqwest::Client::new(); + let host = get_host(); + let url = format!("{host}/function/{function_name}/publish"); + let response = client + .put(url) + .header("x-tenant", tenant) + .send() + .await + .map_err(|e| e.to_string())?; + let status = response.status(); + let resp_data = response + .text() + .await + .unwrap_or("Cannot decode response".to_string()); + match status { + StatusCode::OK => Ok(resp_data), + _ => Err(resp_data), + } +} diff --git a/crates/frontend/src/pages/mod.rs b/crates/frontend/src/pages/mod.rs new file mode 100644 index 000000000..84fb72802 --- /dev/null +++ b/crates/frontend/src/pages/mod.rs @@ -0,0 +1,10 @@ +#![allow(non_snake_case)] + +pub mod ContextOverride; +pub mod DefaultConfig; +pub mod Dimensions; +pub mod Experiment; +pub mod Home; +pub mod NotFound; +pub mod experiment_list; +pub mod function; diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs new file mode 100644 index 000000000..b47a25567 --- /dev/null +++ b/crates/frontend/src/types.rs @@ -0,0 +1,207 @@ +use leptos::{ReadSignal, WriteSignal}; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, vec::Vec}; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use derive_more::{Deref, DerefMut}; +use serde_json::{Map, Value}; + +use crate::components::dropdown::utils::DropdownOption; + +#[derive(Clone, Debug)] +pub struct AppRoute { + pub key: String, + pub path: String, + pub icon: String, + pub label: String, +} + +pub type InputVector = Vec<(ReadSignal<String>, WriteSignal<String>)>; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] +pub enum AppEnv { + PROD, + SANDBOX, + DEV, +} + +impl FromStr for AppEnv { + type Err = String; + fn from_str(val: &str) -> Result<AppEnv, Self::Err> { + match val { + "PROD" => Ok(AppEnv::PROD), + "SANDBOX" => Ok(AppEnv::SANDBOX), + "DEV" => Ok(AppEnv::DEV), + _ => Err("invalid app env!!".to_string()), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Envs { + pub host: String, + pub tenants: Vec<String>, + pub service_prefix: &'static str, +} + +/*************************Function Type ***************************/ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionResponse { + pub function_name: String, + pub published_code: Option<String>, + pub draft_code: String, + pub function_description: String, + pub published_runtime_version: Option<String>, + pub draft_runtime_version: String, + pub published_at: Option<NaiveDateTime>, + pub draft_edited_at: NaiveDateTime, + pub published_by: Option<String>, + pub draft_edited_by: String, +} + +/*********************** Experimentation Types ****************************************/ + +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] +#[strum(serialize_all = "UPPERCASE")] +pub enum ExperimentStatusType { + CREATED, + CONCLUDED, + INPROGRESS, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub last_modified: DateTime<Utc>, + + pub name: String, + pub override_keys: Vec<String>, + pub status: ExperimentStatusType, + pub traffic_percentage: i32, + + pub context: Value, + pub variants: Value, + pub chosen_variant: Option<String>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExperimentsResponse { + pub total_items: i64, + pub total_pages: i64, + pub data: Vec<ExperimentResponse>, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, PartialEq)] +pub struct StatusTypes(pub Vec<ExperimentStatusType>); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ListFilters { + pub status: Option<StatusTypes>, + pub from_date: Option<DateTime<Utc>>, + pub to_date: Option<DateTime<Utc>>, + pub page: Option<i64>, + pub count: Option<i64>, +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, strum_macros::Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum VariantType { + CONTROL, + EXPERIMENTAL, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Variant { + pub id: String, + pub variant_type: VariantType, + pub context_id: Option<String>, + pub override_id: Option<String>, + pub overrides: Map<String, Value>, +} + +pub type Variants = Vec<Variant>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Experiment { + pub(crate) variants: Variants, + pub(crate) name: String, + pub(crate) id: String, + pub(crate) traffic_percentage: u8, + pub(crate) context: Value, + pub(crate) status: ExperimentStatusType, + pub(crate) override_keys: Value, + pub(crate) created_by: String, + pub(crate) created_at: DateTime<Utc>, + pub(crate) last_modified: DateTime<Utc>, + pub(crate) chosen_variant: Option<String>, +} + +/*************************** Context-Override types ********************************/ + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Dimension { + pub dimension: String, + pub priority: i32, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, + pub function_name: Option<String>, +} + +impl DropdownOption for Dimension { + fn key(&self) -> String { + self.dimension.clone() + } + fn label(&self) -> String { + self.dimension.clone() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DefaultConfig { + pub key: String, + pub value: Value, + pub created_at: DateTime<Utc>, + pub created_by: String, + pub schema: Value, + pub function_name: Option<String>, +} + +impl DropdownOption for DefaultConfig { + fn key(&self) -> String { + self.key.clone() + } + fn label(&self) -> String { + self.key.clone() + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub id: String, + pub condition: Value, + pub override_with_keys: [String; 1], +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Config { + pub contexts: Vec<Context>, + pub overrides: Map<String, Value>, + pub default_configs: Map<String, Value>, +} + +pub type FunctionsName = String; +impl DropdownOption for FunctionsName { + fn key(&self) -> String { + self.clone() + } + fn label(&self) -> String { + self.clone() + } +} diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs new file mode 100644 index 000000000..fc4cb4faf --- /dev/null +++ b/crates/frontend/src/utils.rs @@ -0,0 +1,345 @@ +use std::env; + +use crate::types::{DefaultConfig, Dimension, Envs}; +use leptos::*; +use serde_json::{Number, Value}; +use std::str::FromStr; +use url::Url; +use wasm_bindgen::JsCast; + +pub fn modal_action(name: &str, action: &str) { + if let Some(window) = web_sys::window() { + if let Some(document) = window.document() { + if let Some(modal) = document.get_element_by_id(name) { + logging::log!("Modal found"); + if let Some(el) = modal.dyn_ref::<web_sys::HtmlDialogElement>() { + if action == "close" { + el.close(); + } else { + let _ = el.show_modal(); + } + } + } else { + logging::log!("Modal element not found"); + } + } + } +} + +pub fn add_prefix(o_str: &str, prefix: &str) -> String { + match prefix { + "" | "/" => o_str.to_owned(), + prefix => o_str.to_owned() + "/" + prefix, + } +} + +pub fn is_server() -> bool { + env::var("SERVICE_NAME").is_ok() +} + +pub fn use_url_base() -> String { + let service_prefix = use_service_prefix(); + match service_prefix.as_str() { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + } +} + +pub fn use_host_server() -> String { + let service_prefix = use_service_prefix(); + if is_server() { + add_prefix("http://localhost:8080", &service_prefix) + } else { + get_host() + } +} + +pub fn get_host() -> String { + let context = use_context::<Envs>(); + let service_prefix = use_service_prefix(); + let host = context + .map(|ctx| ctx.host) + .or_else(|| match js_sys::eval("__APP_ENVS?.host") { + Ok(value) => value + .dyn_into::<js_sys::JsString>() + .expect("host is not a string") + .as_string(), + Err(e) => { + logging::log!("Unable to fetch host from __APP_ENVS: {:?}", e); + None + } + }) + .unwrap_or(String::from("http://localhost:xxxx")); + + add_prefix(&host, &service_prefix) +} + +pub fn get_tenants() -> Vec<String> { + let context = use_context::<Envs>(); + context + .map(|ctx: Envs| ctx.tenants) + .or_else(|| { + let tenant_value = match js_sys::eval("__APP_ENVS?.tenants") { + Ok(value) => value + .dyn_into::<js_sys::Array>() + .expect("tenants is not an array") + .to_vec() + .into_iter() + .map(|tenant| { + tenant.dyn_into::<js_sys::JsString>().ok().map(String::from) + }) + .collect::<Option<Vec<String>>>(), + Err(e) => { + logging::log!("Unable to fetch tenants from __APP_ENVS: {:?}", e); + None + } + }; + tenant_value + }) + .unwrap_or(vec![]) +} + +pub fn use_env() -> Envs { + let context = use_context::<Envs>(); + context + .or_else(|| { + let envs = match js_sys::eval("__APP_ENVS") { + Ok(value) => { + let env_obj = value + .dyn_into::<js_sys::Object>() + .expect("__APP_ENV is not an object"); + let env_str: &'static str = Box::leak( + js_sys::JSON::stringify(&env_obj) + .ok() + .map(String::from) + .unwrap_or(String::new()) + .into_boxed_str(), + ); + let envs = serde_json::from_str::<Envs>(env_str) + .expect("unable to parse to Envs struct"); + Some(envs) + } + Err(e) => { + logging::log!("Unable to fetch __APP_ENVS: {:?}", e); + None + } + }; + envs + }) + .expect("unable to get envs") +} + +pub fn use_service_prefix() -> String { + let context = use_context::<Envs>(); + context + .map(|ctx: Envs| String::from(ctx.service_prefix)) + .or_else(|| { + let service_prefix_value = match js_sys::eval("__APP_ENV?.service_prefix") { + Ok(value) => value.dyn_into::<js_sys::JsString>().map(String::from).ok(), + Err(e) => { + logging::log!( + "Unable to fetch service_prefix from __APP_ENVS: {:?}", + e + ); + None + } + }; + service_prefix_value + }) + .unwrap_or(String::new()) +} + +pub fn get_element_by_id<T>(id: &str) -> Option<T> +where + T: wasm_bindgen::JsCast + Clone, +{ + let option_dom_ele = document().get_element_by_id(id); + logging::log!("DOM element found {:?}", option_dom_ele); + match option_dom_ele { + Some(dom_ele) => dom_ele.dyn_ref::<T>().cloned(), + None => None, + } +} + +pub fn show_modal(id: &str) { + let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); + if let Some(dialog_ele) = option_dialog_ele { + let _ = dialog_ele.show_modal(); + logging::log!("{:?}", dialog_ele); + } +} + +pub fn close_modal(id: &str) { + let option_dialog_ele = get_element_by_id::<web_sys::HtmlDialogElement>(id); + if let Some(dialog_ele) = option_dialog_ele { + let _ = dialog_ele.close(); + logging::log!("{:?}", dialog_ele); + } +} + +pub fn parse_string_to_json_value_vec(input: &str) -> Vec<Value> { + // Parse the input string into a serde_json::Value + let parsed = serde_json::from_str::<Value>(input); + + // Ensure the Value is an Array and convert it to Vec<Value> + match parsed { + Ok(Value::Array(arr)) => arr, + _ => { + logging::log!("Not a valid json in the input"); + vec![] + } + } +} + +pub fn get_variable_name_and_value( + operands: &Vec<Value>, +) -> Result<(&str, String), String> { + let (obj_pos, variable_obj) = operands + .iter() + .enumerate() + .find(|(_, operand)| { + operand.is_object() + && operand + .as_object() + .expect("unable to parse operands as object") + .get("var") + .is_some() + }) + .ok_or(" failed to get variable name from operands list".to_string())?; + + let variable_name = variable_obj + .as_object() + .map_or(None, |obj| obj.get("var")) + .map_or(None, |value| value.as_str()) + .ok_or(" failed to get variable name from operands list".to_string())?; + + let variable_value = operands + .into_iter() + .enumerate() + .filter(|(idx, _)| *idx != obj_pos) + .map(|(_, val)| val.to_string().replace("\"", "")) + .collect::<Vec<String>>() + .join(","); + + Ok((variable_name, variable_value)) +} + +pub fn extract_conditions( + context_json: &Value, +) -> Result<Vec<(String, String, String)>, String> { + // Assuming max 2-level nesting in context json logic + let context = context_json.as_object().ok_or( + "An error occurred while extracting dimensions: context not a valid JSON object" + .to_string(), + )?; + + let conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or("An error occurred while extracting dimensions: failed parsing conditions as an array".to_string())? + .clone(), + None => vec![context_json.clone()], + }; + + let mut condition_tuples = Vec::new(); + for condition in &conditions { + let condition_obj = condition + .as_object() + .ok_or("failed to parse condition as an object".to_string())?; + let operators = condition_obj.keys(); + + for operator in operators { + let operands = condition_obj[operator] + .as_array() + .ok_or("failed to parse operands as an arrays".to_string())?; + + let (variable_name, variable_value) = get_variable_name_and_value(operands)?; + + condition_tuples.push(( + String::from(variable_name), + operator.to_owned(), + variable_value.to_owned(), + )); + } + } + + Ok(condition_tuples) +} + +pub fn check_url_and_return_val(s: String) -> String { + match Url::parse(&s) { + Ok(_) => format!( + "<a class='value_link' href={} target='_blank'>{}</a>", + &s, &s + ), + Err(_) => s, + } +} + +pub enum ConfigType { + DefaultConfig(DefaultConfig), + Dimension(Dimension), +} + +pub enum ConfigValueType { + Boolean, + Number, + String, + Other, +} + +pub fn get_config_type( + configs: &[ConfigType], + key_name: &str, +) -> Option<ConfigValueType> { + let config = configs.iter().find(|conf| match conf { + ConfigType::DefaultConfig(default_conf) => default_conf.key == key_name, + ConfigType::Dimension(dimension) => dimension.dimension == key_name, + }); + + let types_mapping = |type_str: Option<&str>| match type_str { + Some("boolean") => ConfigValueType::Boolean, + Some("number") => ConfigValueType::Number, + Some("string") => ConfigValueType::String, + _ => ConfigValueType::Other, + }; + + config.and_then(|config| match config { + ConfigType::DefaultConfig(default_conf) => default_conf + .schema + .get("type") + .map(|t| types_mapping(t.as_str())), + ConfigType::Dimension(dimension) => dimension + .schema + .get("type") + .map(|t| types_mapping(t.as_str())), + }) +} + +pub fn get_config_value( + name: &str, + val: &str, + configs: &[ConfigType], +) -> Result<Value, String> { + let config_value_type = get_config_type(configs, name); + match config_value_type { + Some(ConfigValueType::Boolean) => bool::from_str(val) + .map(Value::Bool) + .map_err(|_| "Invalid boolean".to_string()), + Some(ConfigValueType::Number) => val + .parse::<i64>() + .map(|number| Value::Number(number.into())) + .or_else(|_| { + f64::from_str(val) + .ok() + .and_then(|num| Number::from_f64(num).map(Value::Number)) + .ok_or_else(|| { + "Invalid decimal format or precision issue".to_string() + }) + }), + Some(ConfigValueType::String) => Ok(Value::String(val.to_string())), + Some(ConfigValueType::Other) | None => { + Value::from_str(val).map_err(|err| format!("Error parsing JSON: {}", err)) + } + } +} diff --git a/crates/frontend/styles/output.css b/crates/frontend/styles/output.css new file mode 100644 index 000000000..9d1acd383 --- /dev/null +++ b/crates/frontend/styles/output.css @@ -0,0 +1,535 @@ +/* +! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} \ No newline at end of file diff --git a/crates/frontend/styles/tailwind.css b/crates/frontend/styles/tailwind.css new file mode 100644 index 000000000..63f6b31de --- /dev/null +++ b/crates/frontend/styles/tailwind.css @@ -0,0 +1,78 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.h-sidenav { + height: calc(100vh - 370px); +} + +.disable-click { + pointer-events: none; + cursor: not-allowed; + filter: opacity(50%); +} + +.menu li > :not(ul):not(.menu-title):not(details).active { + @apply bg-white; + @apply text-black; + @apply shadow-lg; + @apply font-bold; +} + +.context_condition{ + font-size: 0.9rem; +} + +.value_link { + text-decoration:underline; +} + +.drawer-zindex { + z-index: 99999999; +} + +.btn-purple-outline { + @apply text-purple-500; + @apply dark:text-purple-800; + @apply bg-white; + @apply hover:bg-white; + @apply ring-2; + @apply ring-purple-500; + @apply dark:ring-purple-800; +} + +.btn-purple-link { + @apply text-purple-500; + @apply dark:text-purple-800; + @apply bg-transparent; + @apply outline-none; + @apply focus:outline-none; + @apply border-0; + @apply shadow-none; + @apply hover:bg-transparent; +} + +.btn-purple { + @apply text-white; + @apply bg-gradient-to-r; + @apply from-purple-500; + @apply via-purple-600; + @apply to-purple-700; + @apply hover:bg-gradient-to-br; + @apply focus:ring-4; + @apply focus:outline-none; + @apply focus:ring-purple-300; + @apply dark:focus:ring-purple-800; + @apply shadow-lg; + @apply shadow-purple-500/50; + @apply dark:shadow-lg; + @apply dark:shadow-purple-800/80; +} + +.word-break-break { + word-break: break-word; +} + +.min-w-48 { + min-width: 12rem; +} \ No newline at end of file diff --git a/crates/frontend/tailwind.config.js b/crates/frontend/tailwind.config.js new file mode 100644 index 000000000..645217149 --- /dev/null +++ b/crates/frontend/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "*.html", + "./src/app.rs", + "./src/pages/**/*.rs", + "./src/components/**/*.rs", + "./src/hoc/**/*.rs" + ], + theme: { + extend: {}, + }, + plugins: [ + require("daisyui") + ], + daisyui: { + themes: ["light", "dark", "cupcake", "dim"], + }, +} \ No newline at end of file diff --git a/crates/juspay_superposition/Cargo.toml b/crates/juspay_superposition/Cargo.toml new file mode 100644 index 000000000..c3a2e7225 --- /dev/null +++ b/crates/juspay_superposition/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "juspay_superposition" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cac_client = { path = "../cac_client" } +frontend = { path = "../frontend" } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +context_aware_config = { path = "../context_aware_config" } +dashboard-auth = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.6.0" } +tracing-utils = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/sdk-rs-utils.git", version = "0.3.0" } +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +strum_macros = { workspace = true } +strum = { workspace = true } +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +blake3 = { workspace = true} +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +rand = { workspace = true } +tracing-log = "0.1.3" +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" +futures = "0.3.28" +actix-http = "3.3.1" +futures-util = "0.3.28" +external = { path = "../external" } +actix-cors = "0.6.4" +leptos_actix = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +actix-files = { version = "0.6" } +anyhow = { workspace = true } + diff --git a/crates/juspay_superposition/src/auth.rs b/crates/juspay_superposition/src/auth.rs new file mode 100644 index 000000000..e58c2f152 --- /dev/null +++ b/crates/juspay_superposition/src/auth.rs @@ -0,0 +1,193 @@ +use dashboard_auth::types::AuthenticatedRoute; + +pub fn fill_service_prefix( + routes: Vec<(&'static str, AuthenticatedRoute)>, + service_prefix: &str, +) -> Vec<(String, AuthenticatedRoute)> { + routes + .into_iter() + .map(|route| { + ( + route.0.replace("{service_prefix}", service_prefix), + route.1.clone(), + ) + }) + .collect() +} + +pub mod experiments { + use dashboard_auth::types::AuthenticatedRoute; + + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "POST::{service_prefix}/experiments", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::{service_prefix}/experiments/{experiment_id}/conclude", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::{service_prefix}/experiments/{id}/ramp", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::{service_prefix}/experiments/{id}/stabilize", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::{service_prefix}/experiments/{id}/revert", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/experiments/{id}/overrides", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} + +pub mod contexts { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "PUT::{service_prefix}/context", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/context/move/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "DELETE::{service_prefix}/context/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/context/{ctx_id}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/context/bulk-operations", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} + +pub mod default_config { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([( + "PUT::{service_prefix}/default-config/{key}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + )]) + } +} + +pub mod dimension { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([( + "PUT::{service_prefix}/dimension", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + )]) + } +} + +pub mod functions { + use dashboard_auth::types::AuthenticatedRoute; + pub fn authenticated_routes() -> Vec<(&'static str, AuthenticatedRoute)> { + Vec::from([ + ( + "POST::{service_prefix}/function", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PATCH::{service_prefix}/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "GET::{service_prefix}/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "DELETE::{service_prefix}/function/{function_name}", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "GET::{service_prefix}/function", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/function/{function_name}/{stage}/test", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ( + "PUT::{service_prefix}/function/{function_name}/publish", + AuthenticatedRoute { + api_tag: "MANAGER".into(), + user_permissions: ("manager".into(), "RW".into()), + }, + ), + ]) + } +} diff --git a/crates/juspay_superposition/src/logger.rs b/crates/juspay_superposition/src/logger.rs new file mode 100644 index 000000000..d0caf0717 --- /dev/null +++ b/crates/juspay_superposition/src/logger.rs @@ -0,0 +1,70 @@ +use actix_http::header::{HeaderMap, HeaderValue}; +use actix_web::{ + body::MessageBody, + dev::{ServiceRequest, ServiceResponse}, + Error, +}; +use service_utils::helpers::get_pod_info; +use tracing_utils::tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder}; +use tracing_utils::tracing_subscriber::filter::EnvFilter; +use tracing_utils::{ + tracing::{span, Level, Span}, + tracing_actix_web, +}; + +pub struct CustomRootSpanBuilder; + +impl RootSpanBuilder for CustomRootSpanBuilder { + fn on_request_start(request: &ServiceRequest) -> Span { + let (pod_identifier, deployment_id) = get_pod_info(); + let headers = request.headers(); + let extractor_header = |headers: &HeaderMap, key: &str, default: &str| { + headers + .get(key) + .map(HeaderValue::to_str) + .unwrap_or(Ok(default)) + .unwrap_or(default) + .to_owned() + }; + let tenant = extractor_header(headers, "x-tenant", "no-tenant-header"); + let user_agent = extractor_header(headers, "user-agent", "no-user-agent"); + let method = request.method().to_string(); + let path = request.path(); + tracing_actix_web::root_span!( + request, + service = "context-aware-config", + tenant, + user_agent, + method, + path, + pod_id = pod_identifier, + deployment_id = deployment_id + ) + } + + fn on_request_end<B: MessageBody>( + span: Span, + outcome: &Result<ServiceResponse<B>, Error>, + ) { + DefaultRootSpanBuilder::on_request_end(span, outcome); + let (pod_identifier, deployment_id) = get_pod_info(); + let cac_span = span!( + Level::INFO, + "app", + service = "context-aware-config", + pod_id = pod_identifier, + deployment_id = deployment_id + ); + let _span_entered = cac_span.enter(); + } +} + +pub fn init_log_subscriber() { + let subscriber = tracing_utils::tracing_subscriber::fmt::Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()); + if Ok(String::from("DEV")) == std::env::var("APP_ENV") { + subscriber.compact().init(); + } else { + subscriber.json().init(); + } +} diff --git a/crates/juspay_superposition/src/main.rs b/crates/juspay_superposition/src/main.rs new file mode 100644 index 000000000..c16e6faf6 --- /dev/null +++ b/crates/juspay_superposition/src/main.rs @@ -0,0 +1,319 @@ +mod auth; +mod logger; +mod middlewares; + +use actix_web::{ + web::{self, get, scope, Data}, + App, HttpResponse, HttpServer, +}; +use auth::fill_service_prefix; +use context_aware_config::api::*; +use context_aware_config::helpers::{ + get_default_config_validation_schema, get_meta_schema, +}; +use context_aware_config::middlewares::cookie_to_header::CookieToHeader; +use dashboard_auth::{ + middleware::DashboardAuth, + types::{AuthenticatedRoute, AuthenticatedRouteList}, +}; +use dotenv; +use experimentation_platform::api::*; +use logger::{init_log_subscriber, CustomRootSpanBuilder}; +use std::{collections::HashSet, io::Result}; +use tracing::{span, Level}; + +use snowflake::SnowflakeIdGenerator; +use std::{sync::Mutex, time::Duration}; +use tracing_utils::{tracing_actix_web::TracingLogger, GoldenSignalFactory}; + +use actix_files::Files; +use frontend::app::*; +use frontend::types::Envs as UIEnvs; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; +use service_utils::{ + db::{pgschema_manager::PgSchemaManager, utils::init_pool_manager}, + helpers::{get_from_env_or_default, get_from_env_unsafe, get_pod_info}, + middlewares::{ + app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, + }, + service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, +}; + +#[actix_web::get("favicon.ico")] +async fn favicon( + leptos_options: actix_web::web::Data<leptos::LeptosOptions>, +) -> actix_web::Result<actix_files::NamedFile> { + let leptos_options = leptos_options.into_inner(); + let site_root = &leptos_options.site_root; + Ok(actix_files::NamedFile::open(format!( + "{site_root}/favicon.ico" + ))?) +} + +#[actix_web::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + init_log_subscriber(); + let (pod_identifier, deployment_id) = get_pod_info(); + let cac_span = span!( + Level::INFO, + "app", + service = "context-aware-config", + pod_id = pod_identifier, + deployment_id = deployment_id + ); + let _span_entered = cac_span.enter(); + let service_prefix: String = + get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set"); + + /* + Reading from a env returns a String at best we cannot obtain a &'static str from it, + which seems logical as it not known at compiletime, and there is no straightforward way to do this. + + Leptos' Router component base prop type is &'static str, since service_prefix is of String type + we cannot give this as base value. + + This can be solved, if somehow we can tell rust that this String is going to live for entirety of the process, + here comes Box::leak() to our rescue, which keeps the value in the memory for the entire process lifetime, + this also enables to borrow the String value as &'static str . + */ + let service_prefix_str: &'static str = Box::leak(service_prefix.into_boxed_str()); + let base = match service_prefix_str { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + + let cac_port: u16 = get_from_env_unsafe("PORT").unwrap_or(8080); + let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); + let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") + .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); + + let api_host: String = + get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); + let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); + let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") + .expect("ENABLE_TENANT_AND_SCOPE is not set"); + let tenants: HashSet<String> = get_from_env_unsafe::<String>("TENANTS") + .expect("TENANTS is not set") + .split(",") + .map(|tenant| tenant.to_string()) + .collect::<HashSet<String>>(); + let tenant_middleware_exclusion_list = + get_from_env_unsafe::<String>("TENANT_MIDDLEWARE_EXCLUSION_LIST") + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(",") + .map(String::from) + .collect::<HashSet<String>>(); + + let string_to_int = |s: &String| -> i32 { + s.chars() + .map(|i| (i as i32) & rand::random::<i32>()) + .fold(0, i32::wrapping_add) + }; + + let schema_manager: PgSchemaManager = init_pool_manager( + tenants.clone(), + enable_tenant_and_scope, + app_env, + max_pool_size, + ) + .await; + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + let allow_same_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); + let allow_diff_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); + let allow_same_keys_non_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + /* Frontend configurations */ + let ui_redirect_path = match tenants.iter().next() { + Some(tenant) => format!("{}/admin/{}/resolve", base, tenant), + None => String::from("/admin"), + }; + + let ui_envs = UIEnvs { + service_prefix: service_prefix_str, + tenants: tenants.clone().into_iter().collect::<Vec<String>>(), + host: api_host.clone(), + }; + + let routes_ui_envs = ui_envs.clone(); + + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); + // Generate the list of routes in your Leptos App + let routes = generate_route_list(move || { + return view! { <App app_envs=routes_ui_envs.clone()/> }; + }); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + let leptos_envs = ui_envs.clone(); + let cac_host = cac_host.to_owned() + base.as_str(); + App::new() + .wrap(DashboardAuth::default(authenticated_routes(base.as_str()))) + .wrap(DashboardAuth::default(authenticated_routes(""))) + .wrap(TenantMiddlewareFactory) + .wrap(middlewares::cors()) + .wrap(GoldenSignalFactory::with_blacklist(vec![])) + .wrap(TracingLogger::<CustomRootSpanBuilder>::new()) + .app_data(Data::new(AppState { + db_pool: schema_manager.clone(), + default_config_validation_schema: get_default_config_validation_schema(), + cac_host: cac_host.to_owned(), + cac_version: cac_version.to_owned(), + + experimentation_flags: ExperimentationFlags { + allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx + .to_owned(), + allow_diff_keys_overlapping_ctx: allow_diff_keys_overlapping_ctx + .to_owned(), + allow_same_keys_non_overlapping_ctx: + allow_same_keys_non_overlapping_ctx.to_owned(), + }, + + snowflake_generator: Mutex::new(SnowflakeIdGenerator::new( + string_to_int(&deployment_id), + string_to_int(&pod_identifier), + )), + meta_schema: get_meta_schema(), + app_env: app_env.to_owned(), + enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), + tenants: tenants.to_owned(), + tenant_middleware_exclusion_list: tenant_middleware_exclusion_list + .to_owned(), + service_prefix: service_prefix_str.to_owned(), + })) + .wrap( + actix_web::middleware::DefaultHeaders::new() + .add(("X-SERVER-VERSION", cac_version.to_string())) + .add(("X-DEPLOYMENT-ID", deployment_id.clone())) + .add(("X-POD-ID", pod_identifier.clone())), + ) + .wrap(CookieToHeader) + .service(web::redirect("/", ui_redirect_path.to_string())) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + move || view! { <App app_envs=leptos_envs.clone()/> }, + ) + .service( + scope(&base) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) + .service( + external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) + /***************************** UI Routes ******************************/ + .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}"))) + // serve the favicon from /favicon.ico + ) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) + .service( + external::endpoints(experiments::endpoints(scope("/experiments"))).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) + .app_data(Data::new(leptos_options.to_owned())) + }) + .bind(("0.0.0.0", cac_port))? + .workers(5) + .keep_alive(Duration::from_secs( + get_from_env_unsafe("ACTIX_KEEP_ALIVE").unwrap_or(120), + )) + .run() + .await +} + +fn authenticated_routes(service_prefix: &str) -> AuthenticatedRouteList { + let mut route_vector: Vec<(&str, AuthenticatedRoute)> = Vec::new(); + route_vector.append(&mut auth::contexts::authenticated_routes()); + route_vector.append(&mut auth::default_config::authenticated_routes()); + route_vector.append(&mut auth::dimension::authenticated_routes()); + route_vector.append(&mut auth::experiments::authenticated_routes()); + route_vector.append(&mut auth::functions::authenticated_routes()); + AuthenticatedRouteList::from(fill_service_prefix(route_vector, service_prefix)) +} diff --git a/crates/juspay_superposition/src/middlewares/mod.rs b/crates/juspay_superposition/src/middlewares/mod.rs new file mode 100644 index 000000000..7c8f2b34c --- /dev/null +++ b/crates/juspay_superposition/src/middlewares/mod.rs @@ -0,0 +1,23 @@ +use actix_web::{dev::RequestHead, http::header::HeaderValue}; +pub fn cors() -> actix_cors::Cors { + let origins_env_name = "MJOS_ALLOWED_ORIGINS"; + let allowed_origins: Vec<String> = std::env::var(origins_env_name) + .expect(&format!("{origins_env_name} env")) + .split(",") + .map(String::from) + .collect(); + let validate_origin = move |req_origin: &HeaderValue, _req: &RequestHead| { + if let Ok(req_origin) = req_origin.to_str() { + allowed_origins.contains(&req_origin.into()) + } else { + log::error!("string parsing of req_origin {req_origin:?} failed"); + false + } + }; + actix_cors::Cors::default() + //TODO move this to allowed_origin_fn once middlewares which put tenant + //in request extension are attached + .allowed_origin_fn(validate_origin) + .allow_any_method() + .allow_any_header() +} diff --git a/crates/service_utils/CHANGELOG.md b/crates/service_utils/CHANGELOG.md new file mode 100644 index 000000000..076ad9782 --- /dev/null +++ b/crates/service_utils/CHANGELOG.md @@ -0,0 +1,135 @@ +# Changelog +All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. + +- - - +## service_utils-v0.13.0 - 2024-04-10 +#### Features +- [PICAF-25423] added new result, error type and error macros - (e673fb1) - Shubhranshu Sanjeev +#### Refactoring +- [PICAF-26558] refactored service to use new error type and better error handling - (741f391) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.12.0 - 2024-03-08 +#### Features +- PICAF-25884 Added function validation for context and default_config - (990b729) - ankit.mahato + +- - - + +## service_utils-v0.11.0 - 2024-02-20 +#### Features +- support for service prefix - (a2915b4) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.10.3 - 2024-02-15 +#### Bug Fixes +- fixing error message for experiment create and bulk context api - (bc0d7be) - Jenkins + +- - - + +## service_utils-v0.10.2 - 2024-01-22 +#### Bug Fixes +- fixed host resolve issue for internal calls in SSR. - (3cc9d6e) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.10.1 - 2024-01-18 +#### Bug Fixes +- error resolving pages with internal call to server - (084d08b) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.10.0 - 2024-01-04 +#### Bug Fixes +- frontend multi-tenancy support + config and dimension page - (a1689a1) - Shubhranshu Sanjeev +#### Features +- experiment UI - (24e1b56) - Kartik Gajendra + +- - - + +## service_utils-v0.9.0 - 2023-11-11 +#### Features +- added format check in the JenkinsFile(PICAF-24813) - (4fdf864) - Saurav Suman +#### Miscellaneous Chores +- [PICAF-24778] move dependencies to workspaces - (38a524f) - Kartik Gajendra + +- - - + +## service_utils-v0.8.0 - 2023-11-08 +#### Bug Fixes +- make sure envs with defaults prevent failure - (aac0303) - Kartik Gajendra +#### Features +- [PICAF-24779] integrate authorize middleware - (4a582f3) - Kartik Gajendra + +- - - + +## service_utils-v0.7.1 - 2023-10-27 +#### Bug Fixes +- fixed failing health check (x-tenant header not set) - (23af679) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.7.0 - 2023-10-25 +#### Features +- added multi-tenant support - (5d34e78) - Shubhranshu Sanjeev +- added middleware and FromRequest for tenant and app scope info - (07a64ad) - Shubhranshu Sanjeev + +- - - + +## service_utils-v0.6.0 - 2023-10-20 +#### Features +- PICAF-23643 - Dimension value schema validation on context-addition - (b2fad9e) - Prasanna P + +- - - + +## service_utils-v0.5.0 - 2023-10-09 +#### Features +- server's keep-alive time and db connection pool max size made configurable - (110ee00) - Ritick Madaan + +- - - + +## service_utils-v0.4.1 - 2023-10-05 +#### Bug Fixes +- [PICAF-24563] add user struct in delete context API - (9a0360d) - Kartik Gajendra + +- - - + +## service_utils-v0.4.0 - 2023-09-12 +#### Features +- Schema addition for Dimension values - (7960a67) - Prasanna P + +- - - + +## service_utils-v0.3.0 - 2023-09-06 +#### Features +- [PICAF-24065] added pod information in response headers and logs - (5ee8a9c) - Kartik Gajendra + +- - - + +## service_utils-v0.2.0 - 2023-09-05 +#### Features +- [PICAF-24073] add audit log search endpoint - (19f75c7) - Kartik Gajendra +#### Revert +- Revert "fix: PICAF-24114 logged env variable's value before kms decrypting" - (2a935c9) - Ritick Madaan + +- - - + +## service_utils-v0.1.0 - 2023-09-01 +#### Bug Fixes +- PICAF-24114 logged env variable's value before kms decrypting - (5bda6fb) - Ritick Madaan +- added middleware to insert version in response headers - (449eea4) - Shubhranshu Sanjeev +- calling cac apis for creating context - (a7d92f5) - Shubhranshu Sanjeev +#### Continuous Integration +- PICAF-23646 enabling tests in pr builds - (d09f566) - Ritick Madaan +#### Features +- [PICAF-23868] Added Catch all error type for robust error handling - (60f6f2a) - Kartik Gajendra +- [PICAF-23502] added list experiments API - (01b52cc) - Kartik Gajendra +#### Refactoring +- moved fetching db connection in FromRequest trait impl - (c07c1d2) - Shubhranshu Sanjeev +- moved cac to cargo workspaces - (1855ef8) - Shubhranshu Sanjeev +- moved AppState & utility fx to new crate - (4f734a5) - Shubhranshu Sanjeev + +- - - + +Changelog generated by [cocogitto](https://github.com/cocogitto/cocogitto). \ No newline at end of file diff --git a/crates/service_utils/Cargo.toml b/crates/service_utils/Cargo.toml new file mode 100644 index 000000000..d5411c86d --- /dev/null +++ b/crates/service_utils/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "service_utils" +version = "0.13.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +futures-util = "0.3.28" +# To help generate snowflake ids +rs-snowflake = { workspace = true } +#ORM +env_logger = { workspace = true } +anyhow = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } +diesel = { workspace = true } +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +derive_more = { workspace = true } +reqwest = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/service_utils/src/aws/kms.rs b/crates/service_utils/src/aws/kms.rs new file mode 100644 index 000000000..6c4788f99 --- /dev/null +++ b/crates/service_utils/src/aws/kms.rs @@ -0,0 +1,46 @@ +use crate::helpers::get_from_env_unsafe; +use bytes::Bytes; +use rusoto_kms::{DecryptRequest, DecryptResponse, Kms, KmsClient}; +use rusoto_signature::region::Region; + +//TODO refactor below code +#[allow(deprecated)] +pub async fn decrypt(client: KmsClient, secret_name: &str) -> String { + let cypher = get_from_env_unsafe(secret_name) + .map(|x: String| base64::decode(x).unwrap()) + .expect(format!("{secret_name} not found in env").as_str()); + let req = DecryptRequest { + ciphertext_blob: Bytes::from(cypher), + encryption_algorithm: None, + encryption_context: None, + grant_tokens: None, + //NOTE we use symmetric key encryption therefore key_id is optional + key_id: None, + }; + let decrypt_resp = Kms::decrypt(&client, req).await; + match decrypt_resp { + Ok(DecryptResponse { + plaintext: Some(data), + .. + }) => String::from_utf8(data.to_vec()).expect( + format!("Could not convert kms val for {secret_name} to utf8").as_str(), + ), + e => panic!("KMS decryption failed for {secret_name} with error {e:?}"), + } +} + +pub fn new_client() -> KmsClient { + //TODO make this an enum and add to appstate + let app_env: String = get_from_env_unsafe("APP_ENV").unwrap_or(String::from("PROD")); + + let kms_region = match app_env.as_str() { + "DEV" => Region::Custom { + name: get_from_env_unsafe("AWS_REGION").unwrap_or(String::from("ap-south-1")), + endpoint: get_from_env_unsafe("AWS_REGION_ENDPOINT") + .unwrap_or(String::from("http://localhost:4566")), + }, + _ => get_from_env_unsafe("AWS_REGION").unwrap_or(Region::ApSouth1), + }; + + KmsClient::new(kms_region) +} diff --git a/crates/service_utils/src/aws/mod.rs b/crates/service_utils/src/aws/mod.rs new file mode 100644 index 000000000..5550fd59a --- /dev/null +++ b/crates/service_utils/src/aws/mod.rs @@ -0,0 +1 @@ +pub mod kms; diff --git a/crates/service_utils/src/db/mod.rs b/crates/service_utils/src/db/mod.rs new file mode 100644 index 000000000..c3e0aebb8 --- /dev/null +++ b/crates/service_utils/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod pgschema_manager; +pub mod utils; diff --git a/crates/service_utils/src/db/pgschema_manager.rs b/crates/service_utils/src/db/pgschema_manager.rs new file mode 100644 index 000000000..f9e61dbc3 --- /dev/null +++ b/crates/service_utils/src/db/pgschema_manager.rs @@ -0,0 +1,76 @@ +extern crate derive_more; +use derive_more::{Deref, DerefMut, Display}; +use std::collections::HashMap; + +use anyhow::anyhow; +use diesel::{ + r2d2::{ConnectionManager, Pool, PooledConnection}, + PgConnection, +}; + +pub type PgSchemaConnectionPool = Pool<ConnectionManager<PgConnection>>; +pub type PgSchemaConnection = PooledConnection<ConnectionManager<PgConnection>>; + +#[derive(Debug, Display)] +#[display(fmt = "connection config {:?}", self)] +pub struct ConnectionConfig { + name: String, + database_url: String, + schema: String, + count: u32, +} + +impl ConnectionConfig { + pub fn new(name: String, database_url: String, schema: String, count: u32) -> Self { + ConnectionConfig { + name, + database_url, + schema, + count, + } + } + + pub fn conn_url(&self) -> String { + if self.database_url.contains("?") { + format!( + "{}&options=-c%20search_path%3D{},$user,public", + self.database_url, self.schema + ) + } else { + format!( + "{}?options=-c%20search_path%3D{},$user,public", + self.database_url, self.schema + ) + } + } +} + +#[derive(Deref, DerefMut, Clone)] +pub struct PgSchemaManager(HashMap<String, PgSchemaConnectionPool>); + +impl From<Vec<ConnectionConfig>> for PgSchemaManager { + fn from(value: Vec<ConnectionConfig>) -> Self { + let mut schema_manager: PgSchemaManager = PgSchemaManager(HashMap::new()); + for config in value.into_iter() { + let manager = ConnectionManager::<PgConnection>::new(config.conn_url()); + schema_manager.insert( + config.name.clone(), + Pool::builder() + .max_size(config.count) + .build(manager) + .expect(format!("Invalid config provided, {}", config.name).as_str()), + ); + } + schema_manager + } +} + +impl PgSchemaManager { + pub fn get_conn(&self, name: String) -> anyhow::Result<PgSchemaConnection> { + let conn = self + .get(&name) // gets the pool for the given namespace + .ok_or_else(|| anyhow!("Invalid connection name provided: {}", name))? + .get()?; // fetches the connection from the pool + Ok(conn) + } +} diff --git a/crates/service_utils/src/db/utils.rs b/crates/service_utils/src/db/utils.rs new file mode 100644 index 000000000..54405f4ef --- /dev/null +++ b/crates/service_utils/src/db/utils.rs @@ -0,0 +1,65 @@ +use crate::aws::kms; +use crate::db::pgschema_manager::{ConnectionConfig, PgSchemaManager}; +use crate::helpers::{get_from_env_or_default, get_from_env_unsafe}; +use crate::service::types::AppEnv; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; +use std::collections::HashSet; +use urlencoding::encode; + +pub async fn get_database_url() -> String { + let db_user: String = get_from_env_unsafe("DB_USER").unwrap(); + let kms_client = kms::new_client(); + let db_password_raw = kms::decrypt(kms_client, "DB_PASSWORD").await; + let db_password = encode(db_password_raw.as_str()).to_string(); + let db_host: String = get_from_env_unsafe("DB_HOST").unwrap(); + let db_name: String = get_from_env_unsafe("DB_NAME").unwrap(); + format!("postgres://{db_user}:{db_password}@{db_host}/{db_name}") +} + +pub async fn get_pool() -> Pool<ConnectionManager<PgConnection>> { + let db_url = get_database_url().await; + let manager: ConnectionManager<PgConnection> = + ConnectionManager::<PgConnection>::new(db_url); + Pool::builder() + .max_size(get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 3)) + .build(manager) + .expect("Error building a connection pool") +} + +pub async fn init_pool_manager( + tenants: HashSet<String>, + enable_tenant_and_scope: bool, + app_env: AppEnv, + max_pool_size: u32, +) -> PgSchemaManager { + let database_url = get_database_url().await; + let namespaces = match (enable_tenant_and_scope, app_env) { + (true, _) => tenants + .iter() + .flat_map(|tenant| { + [ + format!("{}_cac", tenant), + format!("{}_experimentation", tenant), + ] + }) + .collect::<Vec<String>>(), + (false, _) => vec!["cac_v1".to_string()], + }; + + let connection_configs = namespaces + .iter() + .map(|namespace| { + ConnectionConfig::new( + namespace.to_string(), + database_url.clone(), + namespace.to_string(), + max_pool_size, + ) + }) + .collect::<Vec<ConnectionConfig>>(); + + PgSchemaManager::from(connection_configs) +} diff --git a/crates/service_utils/src/helpers.rs b/crates/service_utils/src/helpers.rs new file mode 100644 index 000000000..70e40d8c9 --- /dev/null +++ b/crates/service_utils/src/helpers.rs @@ -0,0 +1,200 @@ +use actix_web::{error::ErrorInternalServerError, Error}; +use log::info; +use serde::de::{self, IntoDeserializer}; +use std::{ + env::VarError, + fmt::{self, Display}, + str::FromStr, +}; + +use super::result; +use serde_json::{Map, Value}; + +//WARN Do NOT use this fxn inside api requests, instead add the required +//env to AppState and get value from there. As this panics, it should +//only be used for envs needed during app start. +pub fn get_from_env_unsafe<F>(name: &str) -> Result<F, VarError> +where + F: FromStr, + <F as FromStr>::Err: std::fmt::Debug, +{ + std::env::var(name) + .map(|val| val.parse().unwrap()) + .map_err(|e| { + log::info!("{name} env not found with error: {e}"); + return e; + }) +} + +pub fn get_from_env_or_default<F>(name: &str, default: F) -> F +where + F: FromStr + Display, + <F as FromStr>::Err: std::fmt::Debug, +{ + match std::env::var(name) { + Ok(env) => env.parse().unwrap(), + Err(err) => { + info!( + "{name} ENV failed to load due to {err}, using default value {default}" + ); + default + } + } +} + +pub trait ToActixErr<T> { + fn map_err_to_internal_server<B>( + self, + log_prefix: &str, + err_body: B, + ) -> Result<T, Error> + where + B: fmt::Debug + fmt::Display + 'static; +} + +impl<T, E> ToActixErr<T> for Result<T, E> +where + E: fmt::Debug, +{ + fn map_err_to_internal_server<B>( + self, + log_prefix: &str, + err_body: B, + ) -> Result<T, Error> + where + B: fmt::Debug + fmt::Display + 'static, + { + self.map_err(|e| { + log::info!("{log_prefix}, err: {e:?}"); + ErrorInternalServerError(err_body) + }) + } +} + +pub fn deserialize_stringified_list<'de, D, I>( + deserializer: D, +) -> std::result::Result<Vec<I>, D::Error> +where + D: de::Deserializer<'de>, + I: de::DeserializeOwned, +{ + struct StringVecVisitor<I>(std::marker::PhantomData<I>); + + impl<'de, I> de::Visitor<'de> for StringVecVisitor<I> + where + I: de::DeserializeOwned, + { + type Value = Vec<I>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "a string containing comma separated values eg: CREATED,INPROGRESS", + ) + } + + fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E> + where + E: de::Error, + { + let mut query_vector = Vec::new(); + for param in v.split(",") { + let p: I = I::deserialize(param.into_deserializer())?; + query_vector.push(p); + } + Ok(query_vector) + } + } + + deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::<I>)) +} + +pub fn get_pod_info() -> (String, String) { + let hostname: String = get_from_env_unsafe("HOSTNAME").expect("HOSTNAME is not set"); + let tokens = hostname + .split("-") + .map(str::to_string) + .collect::<Vec<String>>(); + let mut tokens = tokens.iter().rev(); + let (pod_id, _replica_set, deployment_id) = ( + tokens.next().unwrap().to_owned(), + tokens.next().unwrap().to_owned(), + tokens.next().unwrap().to_owned(), + ); + (pod_id, deployment_id) +} + +pub fn extract_dimensions(context_json: &Value) -> result::Result<Map<String, Value>> { + // Assuming max 2-level nesting in context json logic + let context = context_json + .as_object() + .ok_or( + result::AppError::BadArgument("Error extracting dimensions, contect not a valid JSON object. Provide a valid JSON context".into()) + )?; + + let conditions = match context.get("and") { + Some(conditions_json) => conditions_json + .as_array() + .ok_or(result::AppError::BadArgument("Error extracting dimensions, failed parsing conditions as an array. Ensure the context provided obeys the rules of JSON logic".into()))? + .clone(), + None => vec![context_json.clone()], + }; + + let mut dimension_tuples = Vec::new(); + for condition in &conditions { + let condition_obj = + condition + .as_object() + .ok_or(result::AppError::BadArgument( + "Failed to parse condition as an object. Ensure the context provided obeys the rules of JSON logic".to_string() + ))?; + let operators = condition_obj.keys(); + + for operator in operators { + let operands = condition_obj[operator].as_array().ok_or(result::AppError::BadArgument( + "Failed to parse operands as an arrays. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; + + let (variable_name, variable_value) = get_variable_name_and_value(operands)?; + + dimension_tuples.push((String::from(variable_name), variable_value.clone())); + } + } + + Ok(Map::from_iter(dimension_tuples)) +} + +pub fn get_variable_name_and_value( + operands: &Vec<Value>, +) -> result::Result<(&str, &Value)> { + let (obj_pos, variable_obj) = operands + .iter() + .enumerate() + .find(|(_, operand)| { + operand.is_object() && operand.as_object().unwrap().get("var").is_some() + }) + .ok_or(result::AppError::BadArgument( + "Failed to get variable name from operands list. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; + + let variable_name = variable_obj + .as_object() + .map_or(None, |obj| obj.get("var")) + .map_or(None, |value| value.as_str()) + .ok_or(result::AppError::BadArgument( + "Failed to get variable name as string. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; + + let value_pos = (obj_pos + 1) % 2; + let variable_value = + operands + .get(value_pos) + .ok_or(result::AppError::BadArgument( + "Failed to get variable value from operands list. Ensure the context provided obeys the rules of JSON logic" + .into() + ))?; + + Ok((variable_name, variable_value)) +} diff --git a/crates/service_utils/src/lib.rs b/crates/service_utils/src/lib.rs new file mode 100644 index 000000000..266b1f465 --- /dev/null +++ b/crates/service_utils/src/lib.rs @@ -0,0 +1,7 @@ +pub mod aws; +pub mod db; +pub mod helpers; +pub mod macros; +pub mod middlewares; +pub mod result; +pub mod service; diff --git a/crates/service_utils/src/macros.rs b/crates/service_utils/src/macros.rs new file mode 100644 index 000000000..1c4f96290 --- /dev/null +++ b/crates/service_utils/src/macros.rs @@ -0,0 +1,58 @@ +#[macro_export] +macro_rules! bad_argument { + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::BadArgument(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::BadArgument($err.to_string()) + }; +} + +#[macro_export] +macro_rules! validation_error { + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::ValidationError(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::ValidationError($err.to_string()) + }; +} + +#[macro_export] +macro_rules! unexpected_error { + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::UnexpectedError(anyhow::anyhow!(format!($msg, $($args)*))) + }; + ($err: tt) => { + service_utils::result::AppError::UnexpectedError(anyhow::anyhow!($err.to_string())) + }; +} + +#[macro_export] +macro_rules! not_found { + ($msg: literal, $($args: tt)*) => { + service_utils::result::AppError::NotFound(format!($msg, $($args)*)) + }; + ($err: tt) => { + service_utils::result::AppError::NotFound($err.to_string()) + }; +} + +#[macro_export] +macro_rules! db_error { + ($error: expr) => { + service_utils::result::AppError::DbError($error) + }; +} + +#[macro_export] +macro_rules! response_error { + ($status: expr, $msg: expr) => { + service_utils::result::AppError::ResponseError( + service_utils::result::ResponseError { + status_code: $status, + message: $msg.to_string(), + }, + ) + }; +} diff --git a/crates/service_utils/src/middlewares/app_scope.rs b/crates/service_utils/src/middlewares/app_scope.rs new file mode 100644 index 000000000..4f6876d0f --- /dev/null +++ b/crates/service_utils/src/middlewares/app_scope.rs @@ -0,0 +1,69 @@ +use std::future::{ready, Ready}; + +use crate::service::types::AppScope; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + Error, HttpMessage, +}; +use futures_util::future::LocalBoxFuture; + +use std::rc::Rc; + +pub struct AppExecutionScopeMiddlewareFactory { + scope: AppScope, +} + +impl AppExecutionScopeMiddlewareFactory { + pub fn new(scope: AppScope) -> Self { + AppExecutionScopeMiddlewareFactory { scope: scope } + } +} + +impl<S, B> Transform<S, ServiceRequest> for AppExecutionScopeMiddlewareFactory +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type InitError = (); + type Transform = AppExecutionScopeMiddleware<S>; + type Future = Ready<Result<Self::Transform, Self::InitError>>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(AppExecutionScopeMiddleware { + service: Rc::new(service), + scope: self.scope.clone(), + })) + } +} + +pub struct AppExecutionScopeMiddleware<S> { + service: Rc<S>, + scope: AppScope, +} + +impl<S, B> Service<ServiceRequest> for AppExecutionScopeMiddleware<S> +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); + let scope = self.scope; + + Box::pin(async move { + req.extensions_mut().insert(scope); + let res = srv.call(req).await?; + Ok(res) + }) + } +} diff --git a/crates/service_utils/src/middlewares/mod.rs b/crates/service_utils/src/middlewares/mod.rs new file mode 100644 index 000000000..2e5d06b8a --- /dev/null +++ b/crates/service_utils/src/middlewares/mod.rs @@ -0,0 +1,2 @@ +pub mod app_scope; +pub mod tenant; diff --git a/crates/service_utils/src/middlewares/tenant.rs b/crates/service_utils/src/middlewares/tenant.rs new file mode 100644 index 000000000..1e1755aca --- /dev/null +++ b/crates/service_utils/src/middlewares/tenant.rs @@ -0,0 +1,145 @@ +use std::future::{ready, Ready}; + +use crate::service::types::{AppState, Tenant}; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + error, + http::header::{HeaderMap, HeaderValue}, + web::Data, + Error, HttpMessage, +}; +use futures_util::future::LocalBoxFuture; +use log::debug; +use std::rc::Rc; + +pub struct TenantMiddlewareFactory; +impl<S, B> Transform<S, ServiceRequest> for TenantMiddlewareFactory +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type InitError = (); + type Transform = TenantMiddleware<S>; + type Future = Ready<Result<Self::Transform, Self::InitError>>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(TenantMiddleware { + service: Rc::new(service), + })) + } +} + +pub struct TenantMiddleware<S> { + service: Rc<S>, +} + +fn extract_tenant_from_header(headers: &HeaderMap) -> Option<&str> { + headers + .get("x-tenant") + .map(|header_value: &HeaderValue| header_value.to_str().ok()) + .flatten() +} + +fn extract_tenant_from_url<'a>( + path: &'a str, + match_pattern: Option<String>, +) -> Option<&'a str> { + match_pattern + .map(move |pattern| { + let pattern_segments = pattern.split("/"); + let path_segments = path.split("/").collect::<Vec<&str>>(); + + debug!("PATTERN_SEGMENTS ===> {:?}", pattern_segments); + debug!("PATH_SEGMENTS ===> {:?}", path_segments); + + std::iter::zip(path_segments, pattern_segments) + .find(|(_, pattern_seg)| pattern_seg == &"{tenant}") + .map(|(path_seg, _)| path_seg) + }) + .flatten() +} + +fn extract_tenant_from_query_params(query_str: &str) -> Option<&str> { + query_str + .split("&") + .find(|segment| segment.contains("tenant=")) + .map(|tenant_query_param| tenant_query_param.split("=").nth(1)) + .flatten() +} + +impl<S, B> Service<ServiceRequest> for TenantMiddleware<S> +where + S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse<B>; + type Error = Error; + type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let srv = self.service.clone(); + + Box::pin(async move { + let app_state = match req.app_data::<Data<AppState>>() { + Some(val) => val, + None => { + log::error!("app state not set"); + return Err(error::ErrorInternalServerError("")); + } + }; + + let base = match app_state.service_prefix.as_str() { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + + let request_path = req.uri().path().replace(&base, ""); + let is_excluded: bool = app_state + .tenant_middleware_exclusion_list + .contains(&request_path); + + if !is_excluded && app_state.enable_tenant_and_scope { + debug!( + "TENANT FROM HEADER ==> {:?}", + extract_tenant_from_header(req.headers()) + ); + debug!( + "TENANT FROM URL ==> {:?}", + extract_tenant_from_url(req.path(), req.match_pattern()) + ); + debug!( + "TENANT FROM QUERY ==> {:?}", + extract_tenant_from_query_params(req.query_string()) + ); + + let tenant = extract_tenant_from_header(req.headers()) + .or_else(|| extract_tenant_from_url(req.path(), req.match_pattern())) + .or_else(|| extract_tenant_from_query_params(req.query_string())); + + let validated_tenant: Tenant = match tenant { + Some(val) if app_state.tenants.contains(val) => { + Tenant(String::from(val)) + } + Some(_) => { + return Err(error::ErrorBadRequest("invalid x-tenant value")); + } + None => { + return Err(error::ErrorBadRequest("x-tenant not set")); + } + }; + + req.extensions_mut().insert(validated_tenant); + } + + let res = srv.call(req).await?; + + Ok(res) + }) + } +} diff --git a/crates/service_utils/src/result.rs b/crates/service_utils/src/result.rs new file mode 100644 index 000000000..90901c54f --- /dev/null +++ b/crates/service_utils/src/result.rs @@ -0,0 +1,121 @@ +use actix_web::{ + error, + http::{header::ContentType, StatusCode}, + HttpResponse, +}; +use derive_more::Display; +use serde::{Deserialize, Serialize}; +use thiserror::Error as this_error; + +#[derive(this_error)] +pub enum AppError { + #[error("validation failed ( `{0}` )")] + ValidationError(String), + #[error("bad arguments ( `{0}` )")] + BadArgument(String), + #[error("not found ( `{0}` )")] + NotFound(String), + #[error(transparent)] + DbError(#[from] diesel::result::Error), + #[error(transparent)] + ResponseError(#[from] ResponseError), + #[error(transparent)] + UnexpectedError(anyhow::Error), +} + +#[derive(Debug, this_error, Display, Clone)] +#[display( + fmt = "server returned an error: {} with status code {}", + message, + status_code +)] +pub struct ResponseError { + pub message: String, + pub status_code: StatusCode, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Display)] +pub struct ErrorResponse { + pub message: String, +} + +pub type Result<T> = core::result::Result<T, AppError>; + +impl AppError { + fn generate_err_response(code: StatusCode, msg: &str) -> HttpResponse { + let response = ErrorResponse { + message: msg.into(), + }; + HttpResponse::build(code) + .insert_header(ContentType::json()) + .json(response) + } +} + +impl error::ResponseError for AppError { + fn error_response(&self) -> HttpResponse { + use diesel::result::DatabaseErrorKind as diesel_error_kind; + use diesel::result::Error as diesel_error; + + log::error!("{}", self); + match self { + AppError::ValidationError(msg) | AppError::BadArgument(msg) => { + Self::generate_err_response(StatusCode::BAD_REQUEST, msg) + } + AppError::NotFound(msg) => { + Self::generate_err_response(StatusCode::NOT_FOUND, msg) + } + AppError::UnexpectedError(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong", + ), + AppError::ResponseError(error) => { + Self::generate_err_response(error.status_code, &error.message) + } + + AppError::DbError(diesel_error::InvalidCString(_)) => { + Self::generate_err_response( + StatusCode::SERVICE_UNAVAILABLE, + "Something went wrong", + ) + } + + AppError::DbError(diesel_error::NotFound) => Self::generate_err_response( + StatusCode::NOT_FOUND, + "No records found. Please refine or correct your search parameters", + ), + + AppError::DbError(diesel_error::DatabaseError( + diesel_error_kind::UniqueViolation + | diesel_error_kind::CheckViolation + | diesel_error_kind::NotNullViolation + | diesel_error_kind::ForeignKeyViolation, + error, + )) => Self::generate_err_response(StatusCode::CONFLICT, error.message()), + + AppError::DbError(_) => Self::generate_err_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Something went wrong", + ), + } + } +} + +fn error_chain_fmt( + e: &dyn std::error::Error, + f: &mut std::fmt::Formatter<'_>, +) -> std::fmt::Result { + writeln!(f, "{}\n", e)?; + let mut current = e.source(); + while let Some(cause) = current { + writeln!(f, "Caused by:\n\t{}", cause)?; + current = cause.source(); + } + Ok(()) +} + +impl std::fmt::Debug for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + error_chain_fmt(self, f) + } +} diff --git a/crates/service_utils/src/service/mod.rs b/crates/service_utils/src/service/mod.rs new file mode 100644 index 000000000..cd408564e --- /dev/null +++ b/crates/service_utils/src/service/mod.rs @@ -0,0 +1 @@ +pub mod types; diff --git a/crates/service_utils/src/service/types.rs b/crates/service_utils/src/service/types.rs new file mode 100644 index 000000000..5f0db5eb3 --- /dev/null +++ b/crates/service_utils/src/service/types.rs @@ -0,0 +1,208 @@ +use crate::db::pgschema_manager::{PgSchemaConnection, PgSchemaManager}; +use derive_more::{Deref, DerefMut}; +use jsonschema::JSONSchema; +use serde_json::json; + +use std::{ + collections::HashSet, + future::{ready, Ready}, + str::FromStr, +}; + +use actix_web::{error, web::Data, Error, FromRequest, HttpMessage}; + +use snowflake::SnowflakeIdGenerator; +use std::sync::Mutex; + +pub struct ExperimentationFlags { + pub allow_same_keys_overlapping_ctx: bool, + pub allow_diff_keys_overlapping_ctx: bool, + pub allow_same_keys_non_overlapping_ctx: bool, +} + +#[derive(Copy, Clone, Debug)] +pub enum AppEnv { + PROD, + SANDBOX, + DEV, +} + +pub struct AppState { + pub cac_host: String, + pub app_env: AppEnv, + pub tenants: HashSet<String>, + pub cac_version: String, + pub db_pool: PgSchemaManager, + pub default_config_validation_schema: JSONSchema, + pub meta_schema: JSONSchema, + pub experimentation_flags: ExperimentationFlags, + pub snowflake_generator: Mutex<SnowflakeIdGenerator>, + pub enable_tenant_and_scope: bool, + pub tenant_middleware_exclusion_list: HashSet<String>, + pub service_prefix: String, +} + +impl FromStr for AppEnv { + type Err = String; + fn from_str(val: &str) -> Result<AppEnv, Self::Err> { + match val { + "PROD" => Ok(AppEnv::PROD), + "SANDBOX" => Ok(AppEnv::SANDBOX), + "DEV" => Ok(AppEnv::DEV), + _ => Err("invalid app env!!".to_string()), + } + } +} + +#[derive(Copy, Clone, Debug, strum_macros::Display)] +#[strum(serialize_all = "lowercase")] +pub enum AppScope { + CAC, + EXPERIMENTATION, +} +impl FromRequest for AppScope { + type Error = Error; + type Future = Ready<Result<Self, Self::Error>>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let scope = req.extensions().get::<AppScope>().cloned(); + let result = match scope { + Some(v) => Ok(v), + None => Err(error::ErrorInternalServerError("app scope not set")), + }; + ready(result) + } +} + +#[derive(Deref, DerefMut, Clone, Debug)] +pub struct AppExecutionNamespace(pub String); +impl AppExecutionNamespace { + pub fn from_request_sync(req: &actix_web::HttpRequest) -> Result<Self, Error> { + let app_state = match req.app_data::<Data<AppState>>() { + Some(val) => val, + None => { + log::error!("get_app_execution_namespace: AppState not set"); + return Err(error::ErrorInternalServerError("")); + } + }; + + let tenant = req.extensions().get::<Tenant>().cloned(); + let scope = req.extensions().get::<AppScope>().cloned(); + + match ( + app_state.enable_tenant_and_scope, + app_state.app_env, + tenant, + scope, + ) { + (false, _, _, _) => Ok(AppExecutionNamespace("cac_v1".to_string())), + (true, _, Some(t), Some(s)) => Ok(AppExecutionNamespace(format!( + "{}_{}", + t.as_str(), + s.to_string() + ))), + (true, _, None, _) => { + log::error!( + "get_app_execution_namespace: Tenant not set in request extensions" + ); + Err(error::ErrorInternalServerError("")) + } + (true, _, _, None) => { + log::error!( + "get_app_execution_namespace: AppScope not set in request extensions" + ); + Err(error::ErrorInternalServerError("")) + } + } + } +} + +impl FromRequest for AppExecutionNamespace { + type Error = Error; + type Future = Ready<Result<Self, Self::Error>>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + ready(AppExecutionNamespace::from_request_sync(req)) + } +} + +#[derive(Deref, DerefMut, Clone, Debug)] +pub struct Tenant(pub String); +impl FromRequest for Tenant { + type Error = Error; + type Future = Ready<Result<Self, Self::Error>>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let tenant = req.extensions().get::<Tenant>().cloned(); + let result = match tenant { + Some(v) => Ok(v), + None => { + let app_state = match req.app_data::<Data<AppState>>() { + Some(val) => val, + None => { + log::error!("app state not set"); + return ready(Err(error::ErrorInternalServerError(json!({ + "message": "an unknown error occurred with the app. Please contact an admin" + })))); + } + }; + if app_state.enable_tenant_and_scope { + Err(error::ErrorInternalServerError(json!({ + "message": "tenant was not set. Please ensure you are passing in the x-tenant header" + }))) + } else { + Ok(Tenant("mjos".into())) + } + } + }; + ready(result) + } +} + +#[derive(Deref, DerefMut)] +pub struct DbConnection(pub PgSchemaConnection); +impl FromRequest for DbConnection { + type Error = Error; + type Future = Ready<Result<DbConnection, Self::Error>>; + + fn from_request( + req: &actix_web::HttpRequest, + _: &mut actix_web::dev::Payload, + ) -> Self::Future { + let namespace = match AppExecutionNamespace::from_request_sync(req) { + Ok(val) => val.as_str().to_string(), + Err(e) => { + return ready(Err(e)); + } + }; + + let app_state = match req.app_data::<Data<AppState>>() { + Some(state) => state, + None => { + log::info!( + "DbConnection-FromRequest: Unable to get app_data from request" + ); + return ready(Err(error::ErrorInternalServerError(""))); + } + }; + + let result = match app_state.db_pool.get_conn(namespace) { + Ok(conn) => Ok(DbConnection(conn)), + Err(e) => { + log::info!("Unable to get db connection from pool, error: {e}"); + Err(error::ErrorInternalServerError("")) + } + }; + + ready(result) + } +} diff --git a/crates/superposition/Cargo.toml b/crates/superposition/Cargo.toml new file mode 100644 index 000000000..3b1997bed --- /dev/null +++ b/crates/superposition/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "superposition" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cac_client = { path = "../cac_client" } +frontend = { path = "../frontend" } +service_utils = { path = "../service_utils" } +experimentation_platform = { path = "../experimentation_platform" } +context_aware_config = { path = "../context_aware_config" } +superposition_types = { git = "ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git", version = "0.1.0" } +# env +dotenv = { workspace = true } +# Https server framework +actix = { workspace = true } +actix-web = { workspace = true } +# To help generate snowflake ids +rs-snowflake = { workspace = true } +# To help with generating uuids +uuid = { workspace = true } +# To serialize and deserialize objects from json +serde = { workspace = true } +serde_json = { workspace = true } +# For logging and debugging +env_logger = { workspace = true } +log = { workspace = true } +# to work with enums +strum_macros = { workspace = true } +strum = { workspace = true } +derive_more = { workspace = true } +# date and time +chrono = { workspace = true } +# ORM +diesel = { workspace = true } +blake3 = { workspace = true} +rusoto_kms = { workspace = true } +rusoto_signature = { workspace = true } +bytes = { workspace = true } +rusoto_core = { workspace = true } +base64 = { workspace = true } +diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } +urlencoding = { workspace = true } +jsonschema = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +rand = { workspace = true } +tracing-log = "0.1.3" +valuable = { version = "0.1.0", features = ["std", "alloc", "derive"]} +itertools = "0.10.5" +futures = "0.3.28" +actix-http = "3.3.1" +futures-util = "0.3.28" +actix-cors = "0.6.4" +leptos_actix = { version = "0.5.2" } +leptos = { workspace = true } +leptos_meta = { workspace = true } +leptos_router = { workspace = true } +actix-files = { version = "0.6" } +anyhow = { workspace = true } diff --git a/crates/superposition/src/main.rs b/crates/superposition/src/main.rs new file mode 100644 index 000000000..1975d1b69 --- /dev/null +++ b/crates/superposition/src/main.rs @@ -0,0 +1,246 @@ +use actix_web::dev::Service; +use actix_web::HttpMessage; +use actix_web::{web, web::get, web::scope, web::Data, App, HttpResponse, HttpServer}; +use context_aware_config::api::*; +use context_aware_config::helpers::{ + get_default_config_validation_schema, get_meta_schema, +}; +use dotenv; +use experimentation_platform::api::*; +use std::{collections::HashSet, io::Result}; +use superposition_types::User; + +use snowflake::SnowflakeIdGenerator; +use std::{sync::Mutex, time::Duration}; + +use actix_files::Files; +use frontend::app::*; +use frontend::types::Envs as UIEnvs; +use leptos::*; +use leptos_actix::{generate_route_list, LeptosRoutes}; +use service_utils::{ + db::pgschema_manager::PgSchemaManager, + db::utils::init_pool_manager, + helpers::{get_from_env_or_default, get_from_env_unsafe}, + middlewares::{ + app_scope::AppExecutionScopeMiddlewareFactory, tenant::TenantMiddlewareFactory, + }, + service::types::{AppEnv, AppScope, AppState, ExperimentationFlags}, +}; + +#[actix_web::get("favicon.ico")] +async fn favicon( + leptos_options: actix_web::web::Data<leptos::LeptosOptions>, +) -> actix_web::Result<actix_files::NamedFile> { + let leptos_options = leptos_options.into_inner(); + let site_root = &leptos_options.site_root; + Ok(actix_files::NamedFile::open(format!( + "{site_root}/favicon.ico" + ))?) +} + +#[actix_web::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + env_logger::init(); + let service_prefix: String = + get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set"); + + /* + Reading from a env returns a String at best we cannot obtain a &'static str from it, + which seems logical as it not known at compiletime, and there is no straightforward way to do this. + + Leptos' Router component base prop type is &'static str, since service_prefix is of String type + we cannot give this as base value. + + This can be solved, if somehow we can tell rust that this String is going to live for entirety of the process, + here comes Box::leak() to our rescue, which keeps the value in the memory for the entire process lifetime, + this also enables to borrow the String value as &'static str . + */ + let service_prefix_str: &'static str = Box::leak(service_prefix.into_boxed_str()); + let base = match service_prefix_str { + "" | "/" => "".to_owned(), + prefix => "/".to_owned() + prefix, + }; + + let cac_host: String = get_from_env_unsafe("CAC_HOST").expect("CAC host is not set"); + let cac_port: u16 = get_from_env_unsafe("PORT").unwrap_or(8080); + let cac_version: String = get_from_env_unsafe("CONTEXT_AWARE_CONFIG_VERSION") + .expect("CONTEXT_AWARE_CONFIG_VERSION is not set"); + let max_pool_size = get_from_env_or_default("MAX_DB_CONNECTION_POOL_SIZE", 2); + + let api_host: String = + get_from_env_unsafe("API_HOSTNAME").expect("API_HOSTNAME is not set"); + let app_env: AppEnv = get_from_env_unsafe("APP_ENV").expect("APP_ENV is not set"); + let enable_tenant_and_scope: bool = get_from_env_unsafe("ENABLE_TENANT_AND_SCOPE") + .expect("ENABLE_TENANT_AND_SCOPE is not set"); + let tenants: HashSet<String> = get_from_env_unsafe::<String>("TENANTS") + .expect("TENANTS is not set") + .split(",") + .map(|tenant| tenant.to_string()) + .collect::<HashSet<String>>(); + let tenant_middleware_exclusion_list = + get_from_env_unsafe::<String>("TENANT_MIDDLEWARE_EXCLUSION_LIST") + .expect("TENANT_MIDDLEWARE_EXCLUSION_LIST is not set") + .split(",") + .map(String::from) + .collect::<HashSet<String>>(); + + let string_to_int = |s: &String| -> i32 { + s.chars() + .map(|i| (i as i32) & rand::random::<i32>()) + .fold(0, i32::wrapping_add) + }; + + let schema_manager: PgSchemaManager = init_pool_manager( + tenants.clone(), + enable_tenant_and_scope, + app_env, + max_pool_size, + ) + .await; + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + let allow_same_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_OVERLAPPING_CTX not set"); + let allow_diff_keys_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_DIFF_KEYS_OVERLAPPING_CTX") + .expect("ALLOW_DIFF_KEYS_OVERLAPPING_CTX not set"); + let allow_same_keys_non_overlapping_ctx: bool = + get_from_env_unsafe("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX") + .expect("ALLOW_SAME_KEYS_NON_OVERLAPPING_CTX not set"); + + /****** EXPERIMENTATION PLATFORM ENVs *********/ + + /* Frontend configurations */ + let ui_redirect_path = match tenants.iter().next() { + Some(tenant) => format!("{}/admin/{}/resolve", base, tenant), + None => String::from("/admin"), + }; + + let ui_envs = UIEnvs { + service_prefix: service_prefix_str, + tenants: tenants.clone().into_iter().collect::<Vec<String>>(), + host: api_host.clone(), + }; + + let routes_ui_envs = ui_envs.clone(); + + let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); + // Generate the list of routes in your Leptos App + let routes = generate_route_list(move || { + return view! { <App app_envs=routes_ui_envs.clone()/> }; + }); + + HttpServer::new(move || { + let leptos_options = &conf.leptos_options; + let site_root = &leptos_options.site_root; + let leptos_envs = ui_envs.clone(); + let cac_host = cac_host.to_owned() + base.as_str(); + App::new() + .wrap_fn(|req, srv| { + let user = User::default(); + req.extensions_mut().insert::<User>(user); + srv.call(req) + }) + .wrap(TenantMiddlewareFactory) + .app_data(Data::new(AppState { + db_pool: schema_manager.clone(), + default_config_validation_schema: get_default_config_validation_schema(), + cac_host: cac_host.to_owned(), + cac_version: cac_version.to_owned(), + + experimentation_flags: ExperimentationFlags { + allow_same_keys_overlapping_ctx: allow_same_keys_overlapping_ctx + .to_owned(), + allow_diff_keys_overlapping_ctx: allow_diff_keys_overlapping_ctx + .to_owned(), + allow_same_keys_non_overlapping_ctx: + allow_same_keys_non_overlapping_ctx.to_owned(), + }, + + snowflake_generator: Mutex::new(SnowflakeIdGenerator::new(1,1)), + meta_schema: get_meta_schema(), + app_env: app_env.to_owned(), + enable_tenant_and_scope: enable_tenant_and_scope.to_owned(), + tenants: tenants.to_owned(), + tenant_middleware_exclusion_list: tenant_middleware_exclusion_list + .to_owned(), + service_prefix: service_prefix_str.to_owned(), + })) + .wrap( + actix_web::middleware::DefaultHeaders::new() + .add(("X-SERVER-VERSION", cac_version.to_string())) + ) + .service(web::redirect("/", ui_redirect_path.to_string())) + .leptos_routes( + leptos_options.to_owned(), + routes.to_owned(), + move || view! { <App app_envs=leptos_envs.clone()/> }, + ) + .service( + scope(&base) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + /***************************** V1 Routes *****************************/ + .service( + scope("/context") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(context::endpoints()), + ) + .service( + scope("/dimension") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(dimension::endpoints()), + ) + .service( + scope("/default-config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(default_config::endpoints()), + ) + .service( + scope("/config") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(config::endpoints()), + ) + .service( + scope("/audit") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(audit_log::endpoints()), + ) + .service( + scope("/function") + .wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::CAC)) + .service(functions::endpoints()), + ) + .service( + experiments::endpoints(scope("/experiments")).wrap( + AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION), + ), + ) + /***************************** UI Routes ******************************/ + .route("/fxn/{tail:.*}", leptos_actix::handle_server_fns()) + // serve JS/WASM/CSS from `pkg` + .service(Files::new("/pkg", format!("{site_root}/pkg"))) + // serve other assets from the `assets` directory + .service(Files::new("/assets", format!("{site_root}"))) + // serve the favicon from /favicon.ico + ) + .route( + "/health", + get().to(|| async { HttpResponse::Ok().body("Health is good :D") }), + ) + .app_data(Data::new(leptos_options.to_owned())) + }) + .bind(("0.0.0.0", cac_port))? + .workers(5) + .keep_alive(Duration::from_secs( + get_from_env_unsafe("ACTIX_KEEP_ALIVE").unwrap_or(120), + )) + .run() + .await +} diff --git a/crates/superposition_types/Cargo.toml b/crates/superposition_types/Cargo.toml new file mode 100644 index 000000000..1bb353762 --- /dev/null +++ b/crates/superposition_types/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "superposition_types" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# env +actix = { workspace = true } +actix-web = { workspace = true } +strum_macros = { workspace = true } +strum = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +derive_more = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/superposition_types/src/lib.rs b/crates/superposition_types/src/lib.rs new file mode 100644 index 000000000..aab543edc --- /dev/null +++ b/crates/superposition_types/src/lib.rs @@ -0,0 +1,75 @@ +use actix::fut::{ready, Ready}; +use actix_web::{dev::Payload, error, FromRequest, HttpMessage, HttpRequest}; +use log::error; +use serde_json::json; + +pub trait SuperpositionUser { + fn get_email(&self) -> String; + fn get_username(&self) -> String; + fn get_auth_token(&self) -> String; + fn get_auth_type(&self) -> String; +} + +#[derive(Debug, Clone)] +pub struct User { + pub email: String, + pub username: String, + pub auth_token: String, + pub auth_type: String, +} + +impl SuperpositionUser for User { + fn get_email(&self) -> String { + self.email.clone() + } + + fn get_username(&self) -> String { + self.username.clone() + } + + fn get_auth_token(&self) -> String { + self.auth_token.clone() + } + + fn get_auth_type(&self) -> String { + self.auth_type.clone() + } +} + +impl Default for User { + fn default() -> Self { + Self { + email: "superposition@juspay.in".into(), + username: "superposition".into(), + auth_token: "1234abcd".into(), + auth_type: "Bearer".into(), + } + } +} + +impl From<Box<dyn SuperpositionUser>> for User { + fn from(value: Box<dyn SuperpositionUser>) -> Self { + User { + email: value.get_email(), + username: value.get_username(), + auth_token: value.get_auth_token(), + auth_type: value.get_auth_type(), + } + } +} + +impl FromRequest for User { + type Error = actix_web::error::Error; + type Future = Ready<Result<Self, Self::Error>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + if let Some(user) = req.extensions().get::<User>() { + ready(Ok(user.to_owned())) + } else { + error!("No user was found while validating token"); + ready(Err(error::ErrorUnauthorized( + json!({"message":"invalid token provided"}), + ))) + } + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 3f210a65f..e221cbec1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,38 +1,26 @@ version: "3.4" -services: - application: - container_name: "application" - command: ./context-aware-config - build: - dockerfile: Dockerfile - context: ./backend - links: - - postgres - depends_on: - - postgres - environment: - - DATABASE_URL=postgres://postgres:docker@postgres:5432/config?sslmode=disable - - DB_POOL_SIZE=10 - - networks: - - library-network - tty: true - ports: - - "8080:8080" - # Middleware +services: postgres: - image: postgres:12 + build: ./docker-compose/postgres/ + container_name: context-aware-config_postgres + ports: + - "5432:5432" environment: POSTGRES_PASSWORD: "docker" POSTGRES_DB: "config" - networks: - - library-network - ports: - - "5432:5432" - - -networks: - library-network: - driver: bridge + restart: on-failure + network_mode: bridge + localstack: + build : ./docker-compose/localstack/ + container_name: context-aware-config_localstack + ports: + - "4510-4559:4510-4559" # external service port range + - "4566:4566" # LocalStack Edge Proxy + - "4571:4571" + network_mode: bridge + environment: + LOCALSTACK_SERVICES: s3, sns, sqs, logs, cloudwatch, kms + AWS_DEFAULT_REGION: ap-south-1 + EDGE_PORT: 4566 diff --git a/docker-compose/localstack/Dockerfile b/docker-compose/localstack/Dockerfile new file mode 100644 index 000000000..e4b84e18b --- /dev/null +++ b/docker-compose/localstack/Dockerfile @@ -0,0 +1,5 @@ +FROM public.ecr.aws/localstack/localstack:1.3.0 + +RUN aws configure set aws_access_key_id test +RUN aws configure set aws_secret_access_key test +RUN aws configure set default.region ap-south-1 \ No newline at end of file diff --git a/docker-compose/localstack/get_db_password.sh b/docker-compose/localstack/get_db_password.sh new file mode 100755 index 000000000..e67449104 --- /dev/null +++ b/docker-compose/localstack/get_db_password.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# CONSTANTS +region="ap-south-1" +alias aws="aws --endpoint-url=http://$DOCKER_DNS:4566 --region=${region}" + +# ****** KMS ******* + +secret_key_id=`aws kms create-key | jq -r .KeyMetadata.KeyId` + +kms_encrypt(){ + echo $(aws kms encrypt --key-id $secret_key_id --plaintext $2 | jq -r .CiphertextBlob) +} + +kms_encrypt DB_PASSWORD docker diff --git a/docker-compose/postgres/Dockerfile b/docker-compose/postgres/Dockerfile new file mode 100644 index 000000000..bfabfe38b --- /dev/null +++ b/docker-compose/postgres/Dockerfile @@ -0,0 +1,4 @@ +FROM public.ecr.aws/docker/library/postgres:12-alpine + +COPY ./db_init.sql /docker-entrypoint-initdb.d/db_init.sql +# CMD ["postgres", "-c", "log_statement=all"] \ No newline at end of file diff --git a/backend/migrations/.keep b/docker-compose/postgres/db_init.sql similarity index 100% rename from backend/migrations/.keep rename to docker-compose/postgres/db_init.sql diff --git a/docs/client-context-aware-configuration.md b/docs/client-context-aware-configuration.md new file mode 100644 index 000000000..6a9bd9f20 --- /dev/null +++ b/docs/client-context-aware-configuration.md @@ -0,0 +1,374 @@ +# Context Aware Config Client Integration +---- + +This provides SDK to interact with ```context-aware-config```. + +- [Context Aware Config Client Integration](#context-aware-config-client-integration) + - [Rust](#rust) + - [Client Factory Methods Reference](#client-factory-methods-reference) + - [Create Client](#create-client) + - [Function definition](#function-definition) + - [Params](#params) + - [Get Client](#get-client) + - [Function definition](#function-definition-1) + - [Params](#params-1) + - [Example Implementation](#example-implementation) + - [CAC Client Methods Reference](#cac-client-methods-reference) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service) + - [Function definition](#function-definition-2) + - [Get Config](#get-config) + - [Funtion Definition](#funtion-definition) + - [Get the last modified Time](#get-the-last-modified-time) + - [Function Definition](#function-definition-3) + - [Evaluate Context to derive configs](#evaluate-context-to-derive-configs) + - [Function Definition](#function-definition-4) + - [Params](#params-2) + - [Get Default Config](#get-default-config) + - [Function Definition](#function-definition-5) + - [Param](#param) + - [Haskell](#haskell) + - [Adding the clients to your project](#adding-the-clients-to-your-project) + - [Nix](#nix) + - [Haskell CAC client functions reference](#haskell-cac-client-functions-reference) + - [Create a client](#create-a-client) + - [Function Definition](#function-definition-6) + - [Param](#param-1) + - [Get a client](#get-a-client) + - [Function Definition](#function-definition-7) + - [Param](#param-2) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service-1) + - [Function definition](#function-definition-8) + - [Param](#param-3) + - [Get Config](#get-config-1) + - [Funtion Definition](#funtion-definition-1) + - [Get the last modified Time](#get-the-last-modified-time-1) + - [Function Definition](#function-definition-9) + - [Evaluate Context to derive configs](#evaluate-context-to-derive-configs-1) + - [Function Definition](#function-definition-10) + - [Params](#params-3) + - [Get Default Config](#get-default-config-1) + - [Function Definition](#function-definition-11) + - [Sample Integration](#sample-integration) + +--- +## Rust + +The rust client have a client factory that helps you work with multiple clients connected to different tenants + +### Client Factory Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client, it is not mandatory to consume the `Ok` value. + +##### Function definition +``` +pub async fn create_client( + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `polling_interval` | Duration | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +pub async fn get_client( + tenant: String + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Example Implementation + +Below is the rust implementation to instantiate CAC client using the client factory. + +```rust +use cac_client as cc; + +let tenants: Vec<String> = ["dev", "test"]; +//You can create a clientFactory +for tenant in tenants { + cc::CLIENT_FACTORY + .create_client( + tenant.to_string(), + update_cac_periodically,//flag for if you want to update cac config periodically + polling_interval,//polling interval in secs, default is 60 + cac_hostname.to_string(),// superposition service host + ) + .await + .expect(format!("{}: Failed to acquire cac_client", tenant).as_str()); +}; +//You can extract an individual tenant's client from clientFactory +let tenant = "dev".to_owned(); +let cac_client = cc::CLIENT_FACTORY.get_client(tenant.clone()).map_err(|e| { + log::error!("{}: {}", tenant.clone(), e); + ErrorType::IgnoreError(format!("{}: Failed to get cac client", tenant)) + })?; +``` + +### CAC Client Methods Reference + +After calling `get_client` method of Client Factory, you can do the following with the `Client` returned. + +#### Run polling for updates from Superposition Service + +the CAC client polls for updates from the superposition service and loads any changes done on the server. This means that configs changed in superposition are reflected on the client in the duration of `polling_interval`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + pub async fn run_polling_updates() + ``` + +#### Get Config + +Get the full config definition of your tenants configuration from superposition. `Config` has the following information: + +``` +pub struct Config { + contexts: Vec<Context>, + overrides: Map<String, Value>, + default_configs: Map<String, Value>, +} +``` + +##### Funtion Definition + +``` +pub fn get_full_config_state_with_filter(query_data: Option<Map<String, Value>>) -> Result<Config, String> +``` + +#### Get the last modified Time + +CAC client lets you get the last modified time of your configs, in case you want to log it, etc. + +##### Function Definition + +``` +pub fn get_last_modified() -> Result<DateTime<Utc>, String> +``` + +#### Evaluate Context to derive configs + +Given a context, get overrides for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. + +##### Function Definition + +``` +pub fn get_resolved_config(context: Map<String, Value>, filter_keys: Option<Vec<String>>) -> Result<Map<String, Value>, String> +``` +##### Params + +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | Map<String, Value> | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `filter_keys` | Option<Vec<String>> | The keys for which you want the values. If empty, all configuration keys are returned | `Some([payment, network, color])` | + +#### Get Default Config + +The default config for a specific set of keys, if provided. If None is provided for `filter_keys`, all configs are returned. + +##### Function Definition + +``` +pub fn get_default_config(filter_keys: Option<Vec<String>>) -> Result<Map<String, Value>, String> +``` +##### Param +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `filter_keys` | Option<Vec<String>> | The keys for which you want the values. If None, all configuration keys are returned | `Some([payment, network, color])` | + +--- + +## Haskell + +### Adding the clients to your project + +#### Nix + +Add the following to your inputs + +``` +crane.url = "github:ipetkov/crane/54b63c8eae4c50172cb50b612946ff1d2bc1c75c"; +crane.inputs.nixpkgs.follows = "common/nixpkgs"; +context-aware-config = { + url = "git+ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config"; + inputs.nixpkgs.follows = "common/nixpkgs"; + inputs.crane.follows = "crane"; +}; +``` + +then, add the following to your imports section in outputs: + +``` +imports = [ + ...... + inputs.context-aware-config.haskellFlakeProjectModules.output +] +``` + +then add the libraries to your project.cabal file: + +``` +extra-libraries: + cac_client + experimentation_client +``` +### Haskell CAC client functions reference + +#### Create a client + +Create a new client in the Client Factory + +##### Function Definition + +``` +createCacClient:: Tenant -> Interval -> Hostname -> IO (Either Error ()) +``` + +##### Param + +| Param | type | description | Example value | +| ---------- | -------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | +| `Interval` | Duration | specifies the time cac client waits before checking with the server for updates, in seconds | 10 | +| `Hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get a client + +Create a new client in the Client Factory + +##### Function Definition + +``` +getCacClient :: Tenant -> IO (Either Error (ForeignPtr CacClient)) +``` + +##### Param + +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------------------------------------------------------------------ | ------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | + + +#### Run polling for updates from Superposition Service + +the CAC client polls for updates from the superposition service and loads any changes done on the server. This means that configs changed in superposition are reflected on the client in the duration of `Interval`. `cacStartPolling` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` +cacStartPolling :: Tenant -> IO () + ``` + +##### Param + +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------------------------------------------------------------------ | ------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `Interval` from `Hostname` | mjos | + +#### Get Config + +Get the full config definition of your tenants configuration from superposition. `Config` has the following information: + +``` +{ + contexts: [Context], + overrides: Map String Value, + default_configs: Map String Value, +} +``` + +##### Funtion Definition + +``` +getFullConfigStateWithFilter :: ForeignPtr CacClient -> Maybe String -> IO (Either Error Value) +``` + +#### Get the last modified Time + +CAC client lets you get the last modified time of your configs, in case you want to log it, etc. + +##### Function Definition + +``` +getCacLastModified :: ForeignPtr CacClient -> IO (Either Error String) +``` + +#### Evaluate Context to derive configs + +Given a context, get overrides for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. + +##### Function Definition + +``` +getResolvedConfig :: ForeignPtr CacClient -> String -> Maybe [String] -> IO (Either Error Value) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ------------------ | ------------------------------------------------------------------------------------- | ----------------------------------------- | +| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `filter_keys` | Maybe([String]) | The keys for which you want the values. If empty, all configuration keys are returned | `Just ([payment, network, color])` | + +#### Get Default Config + +The default config for a specific set of keys, if provided. If Nothing is provided for `filter_keys`, all configs are returned. + +##### Function Definition + +``` +getDefaultConfig :: ForeignPtr CacClient -> Maybe [String] -> IO (Either Error Value) +``` +| Param | type | description | Example value | +| ------ | ----------- | ------------------------------------------------------------------------------------- | --------------------------- | +| `filter_keys` | Maybe([String]) | The keys for which you want the values. If Nothing, all configuration keys are returned | `Just ([payment, network, color])`| + +#### Sample Integration + +``` +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (getResolvedConfig, createCacClient, getCacClient, + getFullConfigStateWithFilter, getCacLastModified, cacStartPolling, getDefaultConfig) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createCacClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkOS (cacStartPolling "dev") + print threadId + getCacClient "dev" >>= \case + Left err -> putStrLn err + Right client -> do + config <- getFullConfigStateWithFilter client Nothing + lastModified <- getCacLastModified client + overrides <- getResolvedConfig client "{\"country\": \"India\"}" $ Just ["country_image_url", "hyperpay_version"] + defaults <- getDefaultConfig client $ Just ["country_image_url", "hyperpay_version"] + filteredConfig <- getFullConfigStateWithFilter client $ Just "{\"prefix\": \"hyperpay\"}" + print config + print lastModified + print overrides + print defaults + print filteredConfig + threadDelay 1000000000 + pure () + +``` \ No newline at end of file diff --git a/docs/client-experimentation.md b/docs/client-experimentation.md new file mode 100644 index 000000000..686fede54 --- /dev/null +++ b/docs/client-experimentation.md @@ -0,0 +1,294 @@ +# Experimentation Client Integration + +This provides SDK to interact with ```experimentation-platform``` + +- [Experimentation Client Integration](#experimentation-client-integration) + - [Rust](#rust) + - [Client Factory Methods Reference](#client-factory-methods-reference) + - [Create Client](#create-client) + - [Function definition](#function-definition) + - [Params](#params) + - [Get Client](#get-client) + - [Function definition](#function-definition-1) + - [Params](#params-1) + - [Example Implementation](#example-implementation) + - [Experiment Client Methods Reference](#experiment-client-methods-reference) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service) + - [Function definition](#function-definition-2) + - [Get an applicable variant](#get-an-applicable-variant) + - [Function Definition](#function-definition-3) + - [Params](#params-2) + - [Get satisfied experiments](#get-satisfied-experiments) + - [Function Definition](#function-definition-4) + - [Params](#params-3) + - [Get all running experiments](#get-all-running-experiments) + - [Function Definition](#function-definition-5) + - [Haskell](#haskell) + - [Experiment Client Methods Reference](#experiment-client-methods-reference-1) + - [Create Client](#create-client-1) + - [Function definition](#function-definition-6) + - [Params](#params-4) + - [Get Client](#get-client-1) + - [Function definition](#function-definition-7) + - [Params](#params-5) + - [Run polling for updates from Superposition Service](#run-polling-for-updates-from-superposition-service-1) + - [Function definition](#function-definition-8) + - [Get an applicable variant](#get-an-applicable-variant-1) + - [Function Definition](#function-definition-9) + - [Params](#params-6) + - [Get satisfied experiments](#get-satisfied-experiments-1) + - [Function Definition](#function-definition-10) + - [Params](#params-7) + - [Get all running experiments](#get-all-running-experiments-1) + - [Function Definition](#function-definition-11) + - [Sample Integration](#sample-integration) + + +## Rust + +The rust client have a client factory that helps you work with multiple clients connected to different tenants + +### Client Factory Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client, it is not mandatory to consume the `Ok` value. + +##### Function definition +``` +pub async fn create_client( + tenant: String, + polling_interval: Duration, + hostname: String, + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `polling_interval` | Duration | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +pub async fn get_client( + tenant: String + ) -> Result<Arc<Client>, String> +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Example Implementation + +Below is the rust implementation to instantiate Experimentation client . + +```rust +use superpostion_client as sp; + +let tenants: Vec<String> = ["dev", "test"]; +//You can create a clientFactory +for tenant in tenants { + rt::spawn( + sp::CLIENT_FACTORY + .create_client(tenant.to_string(), + poll_frequency,//How frequently you want to update config in secs + hostname.to_string()// superposition hostname + ) + .await + .expect(format!("{}: Failed to acquire experimentation_client", tenant).as_str()) + .clone() + .run_polling_updates(), + ); +}; +//You can extract an individual tenant's client from clientFactory +let tenant = "dev".to_owned(); +let sp_client = sp::CLIENT_FACTORY + .get_client(tenant.clone()) + .await + .map_err(|e| { + log::error!("{}: {}", tenant, e); + ErrorType::IgnoreError(format!("{}: Failed to get experimentation_client", tenant)) + })?; + + +``` + +### Experiment Client Methods Reference + +#### Run polling for updates from Superposition Service + +the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client in the duration of `polling_interval`. `run_polling_updates()` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + pub async fn run_polling_updates() + ``` + +#### Get an applicable variant + +When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. + +The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant + +##### Function Definition +``` +pub async fn get_applicable_variant(context: &Value, toss: i8) -> Vec<String> +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `toss` | i8 | an integer that assigns your request to a variant | `4` | + +#### Get satisfied experiments + +Rather than just getting the variant ID, you can get the whole experiment(s) that are satisfying your context - rather than just the final result. + +##### Function Definition +``` +pub async fn get_satisfied_experiments(context: &Value) -> Experiments +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | + +#### Get all running experiments + +Get all running experiments, why would you want to do this? We don't know. But you can. + +##### Function Definition +``` +pub async fn get_running_experiments() -> Experiments +``` + +## Haskell + +### Experiment Client Methods Reference + +#### Create Client + +Create a client in the factory. You can chose to use the result to check for errors faced by the Client Factory while creating your client. + +##### Function definition +``` +createExpClient:: Tenant -> Integer -> String -> IO (Either Error ()) +``` +##### Params +| Param | type | description | Example value | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `Tenant` | String | specifies the tenants configs and contexts that will be loaded into the client at `polling_interval` from `hostname` | mjos | +| `Interval` | Integer | specifies the time cac client waits before checking with the server for updates | Duration::from_secs(5) | +| `Hostname` | String | The URL of the superposition server | https://superposition.example.com | + +#### Get Client + +Get a client + +##### Function definition +``` +getExpClient :: Tenant -> IO (Either Error (ForeignPtr ExpClient)) +``` +##### Params +| Param | type | description | Example value | +| -------- | ------ | ------------------------------------------------ | ------------- | +| `tenant` | String | specifies the tenant used during `create_client` | mjos | + +#### Run polling for updates from Superposition Service + +the Experimentation client polls for updates from the superposition service and loads any changes done on the server. This means that experiments changed in superposition are reflected on the client in the duration of `Interval`. `expStartPolling` should be run in a separate thread, as it does not terminate. + +##### Function definition + + ``` + expStartPolling :: Tenant -> IO () + ``` + +#### Get an applicable variant + +When experiments are running, you can get different variants of the experiment based on the `toss` value you provide. Superposition decides which bucket your request falls into based on this value, and returns an ID called the `variantId`. You can then include this in your CAC client request. + +The toss can be a random number between -1 to 100. You can log the variantId so that your metrics can help you decide on a variant + +##### Function Definition +``` +getApplicableVariants :: ForeignPtr ExpClient -> String -> Integer -> IO (Either Error String) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | String | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | +| `toss` | Integer | an integer that assigns your request to a variant | `4` | + +#### Get satisfied experiments + +Rather than just getting the variant ID, you can get the whole experiment(s) that are satisfying your context - rather than just the final result. + +##### Function Definition +``` +getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> IO (Either Error Value) +``` +##### Params + +| Param | type | description | Example value | +| --------- | ----- | --------------------------------------------------- | ----------------------------------------- | +| `context` | Value | The context under which you want to resolve configs | `{"os": "android", "merchant": "juspay"}` | + +#### Get all running experiments + +Get all running experiments, why would you want to do this? We don't know. But you can. + +##### Function Definition +``` +getRunningExperiments :: ForeignPtr ExpClient -> IO (Either Error Value) +``` + +#### Sample Integration + +``` +{-# LANGUAGE LambdaCase #-} +module Main (main) where + +import Client (createExpClient, expStartPolling, + getApplicableVariants, getExpClient, + getRunningExperiments, + getSatisfiedExperiments) +import Control.Concurrent +import Prelude + +main :: IO () +main = do + createExpClient "dev" 10 "http://localhost:8080" >>= \case + Left err -> putStrLn err + Right _ -> pure () + threadId <- forkIO (expStartPolling "dev") + print threadId + getExpClient "dev" >>= \case + Left err -> putStrLn err + Right client -> loop client + pure () + where + loop client = do + runningExperiments <- getRunningExperiments client + satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" + variants <- getApplicableVariants client "{\"os\": \"android\", \"client\": \"1mg\"}" 9 + print "Running experiments" + print runningExperiments + print "experiments that satisfy context" + print satisfiedExperiments + print "variant ID applied" + print variants + -- threadDelay 10000000 + loop client + +``` \ No newline at end of file diff --git a/docs/context-aware-config.md b/docs/context-aware-config.md new file mode 100644 index 000000000..880466146 --- /dev/null +++ b/docs/context-aware-config.md @@ -0,0 +1,104 @@ +# Context Aware Config +--- + +Context Aware Config (abbreviated as CAC) is the foundational service of the Superposition Platform that is configuration management system that can override config values under certain domain contexts. CAC makes it easy to configure your applications and make sure that you can change these configurations without causing issues in your application + +- [Context Aware Config](#context-aware-config) + - [Concepts](#concepts) + - [Default Configs](#default-configs) + - [Dimensions](#dimensions) + - [Context](#context) + - [Overrides](#overrides) + - [How CAC Works](#how-cac-works) + +## Concepts +--- +### Default Configs + +Default Configs are a fundamental concept to CAC. Default Configs are key value pairs that help CAC model the entity/configuration it is trying to manage and serve. Think of default configs as the best assumption we can make about our configs. + +Example: +Using our car classification example, we can establish defaults like: +1. Number of doors - 4 (most cars do have 4 doors) +2. Engine - Internal Combustion +3. Chassis - Hatchback +4. Color - Silver +5. interiors - leather + +Again remember, these are our base assumptions that we provide to CAC about our configuration + +### Dimensions + +Dimensions are typically attributes of your domain which can potentially govern values that a particular configuration takes. + +Example: + +Let us assume we are modelling a system for buying cars, the dimensions you might use are: + 1. Engine horsepower + 2. manufacturer + 3. model + +Contexts build upon Dimensions + +### Context + +A Context is a logical expression built using dimensions as variables. It takes the form: + +`Dimension operator value` + +Operators are: +- IS: an equality operator +- HAS: similar to the IN operator +- BETWEEN (inclusive): an operator that checks if a provided value is between `value` + +When multiple Context are defined together, they are evaluated as AND - if a context is evaluated as false, then the entire rule will not apply. + +Examples: + +- color IS "red" +- manufacturer IS "hyundai" +- chassis HAS "hatchback" + +### Overrides + +Overrides are a subset of the configuration from Default Config typically with different values. Overrides are always associated with Contexts and are applied when a Context is evaluated to `true`. + +Example: + +lets take an example to configure Tesla cars + +``` +[manufacturer IS "Tesla"] +engine = "EV" + +[manufacturer IS "Tesla", model IS "cybertruck"] +chassis = "truck" + +[manufacturer IS "Tesla", model IS "roadster"] +chassis = "sport" + +[manufacturer IS "Tesla", model IS "Y"] +chassis = "sedan" +``` + +## How CAC Works +--- + +- Your application adds the CAC client as a library +- At runtime, your application will provide a context to the client for evaluation. Using our car example from earlier, the context provided by your application would look like: + + ``` + { + manufacturer: "Tesla", + "model": "Y" + } + ``` +- CAC will apply all contexts it has available, and return the final configuration for the application. If no rule applies for a configuration, it's default value will be returned. From our earlier examples (check Default Config and Overrides section) the configuration returned would be + + ``` + engine = "EV" + chassis = "sedan" + number_of_doors = 4 + color = "silver" + interiors = "leather" + ``` \ No newline at end of file diff --git a/docs/experiment-context-example.png b/docs/experiment-context-example.png new file mode 100644 index 000000000..a6aef6bd3 Binary files /dev/null and b/docs/experiment-context-example.png differ diff --git a/docs/experiment-variant-example.png b/docs/experiment-variant-example.png new file mode 100644 index 000000000..ea63e8b19 Binary files /dev/null and b/docs/experiment-variant-example.png differ diff --git a/docs/experimentation.md b/docs/experimentation.md new file mode 100644 index 000000000..3745f67c2 --- /dev/null +++ b/docs/experimentation.md @@ -0,0 +1,27 @@ +# Experimentation + +## Introduction +In a nutshell, **Experimentation** module enables to run A/B/../n testing for your configurations in equi-sized groups/cohorts, it works on top of **Context-Aware-Config** and can also work as a release system for your configuration. + +## Concepts + +### Experiment +An experiment as name suggest enables you to test and evaluate the behaviour of the system for different values of the same configuration. An experiment can have exactly one **CONTROL** variant and `n` **EXPERIMENTAL** variants, with each variant overriding/changing same set of keys in configuration. An experiment's scope can be controlled by declaring the context, which chalks out the sample set for the experiment from the population. + +<br/> +<br/> + +![context-example](experiment-context-example.png) + +### Variant +A variant in an experiment, represent one of the `n` tests. In simple terms, its a collection of key-value pairs, where each key is your already defined **default-config** key, and the value can be any valid new/old configuration value for the key. There are two kinds of variants: +1. **CONTROL**: It conceptually represents the current state of the configuration. +2. **EXPERIMENTAL**: The experimental variant lets you define the newer value for the **default-config** keys. + +<br/> +<br/> + +![variant-example](experiment-variant-example.png) + +### Experiment's Traffic Percentage +This defines the traffic size for each variant of the experiment, for instance if traffic percentage is `13%` and there are `4` variants in the experiment, this makes each variant of the experiment receive `13%` of the entire traffic and in entirety `13 * 4 = 52%` of the total traffic. diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 000000000..012cd6434 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,78 @@ +# Setup Instructions for Superposition Platform + +This document outlines the setup process for the `Superposition Platform`. + +## Prerequisites + +* Docker installed and running. +* Docker Compose installed. +* [Follow installation instructions at Zero To Nix ↗](https://zero-to-nix.com/start/install) + +# Setup Steps + +## Start docker daemon +```bash + open --background -a Docker + ``` +## Shutdown Local Postgres + +### For HomeBrew: +```bash + # Check if service is running + brew services + # If postgres is running, stop it via brew + brew services stop postgresql@<your_postgres_version> +``` +## Clone the repository +```bash + git clone ssh://git@ssh.bitbucket.juspay.net/picaf/context-aware-config.git + cd context-aware-config +``` +## Build And Run +```bash + nix develop + make setup + make run +``` +## Check Installation + +### Check logs + ```bash + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.291Z","value":"starting 5 workers"} + {"level":"INFO","service":"context-aware-config","timestamp":"2023-08-14T08:08:20.292Z","value":"Actix runtime found; starting in Actix runtime"} +``` +### Check /health endpoint +```bash + curl --location 'http://localhost:8080/health' + # Expected Response : "Health is good :D" +``` + +## Creating New Tenants +```bash + make tenant TENANT=<tenant_name> + # Add the tenant in the TENANTS env variable. For example TENANTS=dev,test,<tenant_name> + # Stop the server and run: + make run + ``` +## Additional Information + +### Make Targets +The following targets are available +* `db-init`: Initializes the database. +* `setup`: Sets up the development environment. +* `kill`: Stops all running containers. +* `run`: Runs the application in development mode. +* `ci-test`: Runs unit tests. +* `ci-build`: Builds the Docker image. +* `ci-push`: Pushes the Docker image to a registry. +* `registry-login`: Logs in to a Docker registry. +* `validate-aws-connection`: Validates the AWS connection. +* `validate-psql-connection`: Validates the PostgreSQL connection. + +### Environment Variables +| Variable | Description | Default Value | +|---|---|---| +| `ENABLE_TENANT_AND_SCOPE` | Enables multi-tenancy | `true` | +| `TENANTS` | List of Tenants | `dev,test` | +| `DOCKER_DNS` | DNS server to use within the container | `localhost` | + diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..02944e91f --- /dev/null +++ b/flake.lock @@ -0,0 +1,172 @@ +{ + "nodes": { + "crane": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1707461758, + "narHash": "sha256-VaqINICYEtVKF0X+chdNtXcNp6poZr385v6AG7j0ybM=", + "owner": "ipetkov", + "repo": "crane", + "rev": "505976eaeac289fe41d074bee37006ac094636bb", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1706830856, + "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "haskell-flake": { + "locked": { + "lastModified": 1707242163, + "narHash": "sha256-w+cBynh7yqnpVtFdu1SEZxPgtlz/nWnv47D5crnPXHM=", + "owner": "srid", + "repo": "haskell-flake", + "rev": "f9d17c3aa68e65529f424816c8b9346ae602d1de", + "type": "github" + }, + "original": { + "owner": "srid", + "repo": "haskell-flake", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1707268954, + "narHash": "sha256-2en1kvde3cJVc3ZnTy8QeD2oKcseLFjYPLKhIGDanQ0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1706550542, + "narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "97b17f32362e475016f942bbdfda4a4a72a8a652", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-parts": "flake-parts", + "haskell-flake": "haskell-flake", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", + "systems": "systems_2" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1707444620, + "narHash": "sha256-P8kRkiJLFttN+hbAOlm11wPxUrQZqKle+QtVCqFiGXY=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "78503e9199010a4df714f29a4f9c00eb2ccae071", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..33852394a --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + haskell-flake.url = "github:srid/haskell-flake"; + systems.url = "github:nix-systems/default"; + crane = { + url = "github:ipetkov/crane"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs: + + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + + imports = [ + inputs.haskell-flake.flakeModule + ./clients/haskell + ./rust.nix + ]; + + perSystem = { pkgs, self', ... }: { + devShells.default = pkgs.mkShell { + inputsFrom = [ + self'.devShells.rust + self'.devShells.haskell + ]; + packages = with pkgs; [ + docker-compose + # Why do we need this? + stdenv.cc + awscli + jq + nodejs_18 + nixpkgs-fmt + ]; + }; + }; + }; +} diff --git a/headers/libcac_client.h b/headers/libcac_client.h new file mode 100644 index 000000000..6a762089f --- /dev/null +++ b/headers/libcac_client.h @@ -0,0 +1,31 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +typedef struct Arc_Client Arc_Client; + +int last_error_length(void); + +const char *last_error_message(void); + +void free_string(char *s); + +int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); + +void start_polling_update(const char *tenant); + +void free_client(struct Arc_Client *ptr); + +struct Arc_Client *get_client(const char *tenant); + +const char *get_last_modified(struct Arc_Client *client); + +const char *get_config(struct Arc_Client *client, const char *query); + +const char *get_resolved_config(struct Arc_Client *client, + const char *query, + const char *filter_keys, + const char *merge_strategy); + +const char *get_default_config(struct Arc_Client *client, const char *filter_keys); diff --git a/headers/libexperimentation_client.h b/headers/libexperimentation_client.h new file mode 100644 index 000000000..f37cbf580 --- /dev/null +++ b/headers/libexperimentation_client.h @@ -0,0 +1,26 @@ +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +typedef struct Arc_Client Arc_Client; + +int last_error_length(void); + +const char *last_error_message(void); + +void free_string(char *s); + +int new_client(const char *tenant, unsigned long update_frequency, const char *hostname); + +void start_polling_update(const char *tenant); + +void free_client(struct Arc_Client *ptr); + +struct Arc_Client *get_client(const char *tenant); + +char *get_applicable_variant(struct Arc_Client *client, const char *c_context, short toss); + +char *get_satisfied_experiments(struct Arc_Client *client, const char *c_context); + +char *get_running_experiments(struct Arc_Client *client); diff --git a/makefile b/makefile index 8c798a52e..0502707c1 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,180 @@ IMAGE_NAME ?= context-aware-config +DOCKER_DNS ?= localhost +TENANT ?= dev +SHELL := /usr/bin/env bash -build: - docker build -f Dockerfile -t $(IMAGE_NAME):$(packageVersion) . +.PHONY: + db-init + setup + kill + run + ci-test + ci-build + ci-push + registry-login + validate-aws-connection + validate-psql-connection + cac -default: build +cleanup: + -docker rm -f $$(docker container ls --filter name=^context-aware-config -a -q) + -docker rmi -f $$(docker images | grep context-aware-config-postgres | cut -f 10 -d " ") + +db-init: + diesel migration run --locked-schema --config-file=crates/context_aware_config/diesel.toml + -diesel migration run --locked-schema --config-file=crates/experimentation_platform/diesel.toml + +cac-migration: cleanup + docker-compose up -d postgres + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection; \ + do echo "waiting for postgres bootup"; \ + sleep 0.5; \ + done + diesel migration run --config-file=crates/context_aware_config/diesel.toml + docker-compose down + +exp-migration: cleanup + docker-compose up -d postgres + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection; \ + do echo "waiting for postgres bootup"; \ + sleep 0.5; \ + done + diesel migration run --config-file=crates/experimentation_platform/diesel.toml + docker-compose down + +migration: cac-migration exp-migration + +legacy_db_setup: + grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/legacy-db-setup.sh + +tenant: + grep 'DATABASE_URL=' .env | sed -e 's/DATABASE_URL=//' | xargs ./scripts/create-tenant.sh $(TENANT) + +validate-aws-connection: + aws --endpoint-url=http://$(DOCKER_DNS):4566 --region=ap-south-1 sts get-caller-identity + +validate-psql-connection: + pg_isready -h $(DOCKER_DNS) -p 5432 + + +env-setup: + npm ci + docker-compose up -d postgres localstack + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + +test-tenant: + make tenant TENANT='test' + +dev-tenant: + make tenant TENANT='dev' + +ci-setup: env-setup test-tenant + npm ci --loglevel=error + make run -e DOCKER_DNS=$(DOCKER_DNS) 2>&1 | tee test_logs & + while ! grep -q "starting in Actix" test_logs; \ + do echo "ci-test: waiting for bootup..." && sleep 4; \ + done + # NOTE: `make db-init` finally starts a postgres container and runs all the migrations with locked-schema option + # to prevent update of schema.rs for both cac and experimentation. + # NOTE: The container spinned-up here is the actual container being used in development + echo setup completed successfully!!! + +setup: migration env-setup test-tenant dev-tenant + # NOTE: `make migration` is being used to run the migrations for cac and experimentation in isolation, + # otherwise the tables and types of cac and experimentation spill into each others schema.rs + # NOTE: The container spinned up are stopped and removed after the work is done. + +kill: + -pkill -f target/debug/juspay_superposition & + +get-password: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh` && echo $$DB_PASSWORD + +juspay_superposition: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo run --color always --bin juspay_superposition --no-default-features --features=ssr + +juspay_superposition_dev: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo watch -x 'run --color always --bin juspay_superposition --no-default-features --features=ssr' + +superposition: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo run --color always --bin superposition --no-default-features --features=ssr + +superposition_dev: + export DB_PASSWORD=`./docker-compose/localstack/get_db_password.sh`; \ + cargo watch -x 'run --color always --bin superposition --no-default-features --features=ssr' + + +frontend: + cd crates/frontend && \ + wasm-pack build --target=web --debug --no-default-features --features=hydrate + cd crates/frontend && \ + npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css + -rm -rf target/site + mkdir target/site && mkdir target/site/pkg + mv crates/frontend/pkg target/site/ + cp -a crates/frontend/assets/. target/site/ + +backend: + -rm -rf target/node_modules + npm --prefix ./crates/context_aware_config/ ci + mv crates/context_aware_config/node_modules target/ + cargo build --color always + +build: frontend backend + +run: kill build + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + make superposition -e DOCKER_DNS=$(DOCKER_DNS) + +juspay_run: kill build + while ! make validate-psql-connection validate-aws-connection; \ + do echo "waiting for postgres, localstack bootup"; \ + sleep 0.5; \ + done + cp .env.example .env + sed -i 's/dockerdns/$(DOCKER_DNS)/g' ./.env + make juspay_superposition -e DOCKER_DNS=$(DOCKER_DNS) + +ci-test: cleanup ci-setup + cargo test + npm run test + rm test_cac.sql + rm test_experimentation.sql + +ci-build: + docker buildx build --ssh default=$(SSH_AUTH_SOCK) \ + -t $(IMAGE_NAME):$(VERSION) \ + --build-arg "CONTEXT_AWARE_CONFIG_VERSION=${VERSION}" \ + --build-arg "SOURCE_COMMIT=${SOURCE_COMMIT}" \ + . + +ci-push: registry-login + docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY_HOST)/$(IMAGE_NAME):$(VERSION) + docker push $(REGISTRY_HOST)/$(IMAGE_NAME):$(VERSION) + +registry-login: + aws ecr get-login-password --region $(REGION) | \ + docker login \ + --username AWS \ + --password-stdin $(REGISTRY_HOST) + +tailwind: + cd crates/frontend && npx tailwindcss -i ./styles/tailwind.css -o ./pkg/style.css --watch + +default: dev-build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 89223b275..4924571ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,31 +1,2582 @@ { - "name": "test", - "version": "0.1", - "lockfileVersion": 1, + "name": "context-aware-configuration", + "version": "0.0.1", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@types/json-logic-js": { + "packages": { + "": { + "name": "context-aware-configuration", + "version": "0.0.1", + "devDependencies": { + "axios": "^0.28.0", + "daisyui": "^4.3.1", + "newman": "https://github.com/knutties/newman.git#feature/newman-dir", + "tailwindcss": "^3.3.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@faker-js/faker": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-5.5.3.tgz", + "integrity": "sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@postman/form-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", + "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@postman/tough-cookie": { + "version": "4.1.3-postman.1", + "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", + "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@postman/tunnel-agent": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.3.tgz", + "integrity": "sha512-k57fzmAZ2PJGxfOA4SGR05ejorHbVAa/84Hxh/2nAztjNXc4ZjOm9NUIk6/Z6LCrBvJZqjRZbN8e/nROVUPVdg==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/axios": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bluebird": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", + "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dev": true, + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/camelcase-css": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.1.tgz", - "integrity": "sha512-exWt46x9L1dSe8xLH/REujkeb5Gcqm1Ygdxukmv2sSVZujJRIl6ARNgq73vONvosiN7miX8gYeBTzxivqiNSgw==", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.6.0.tgz", + "integrity": "sha512-+QOTw3otC4+FxdjK9RopGpNOglADbr4WPFi0SonkO99JbpkTPbMxmdm4NenhF5Zs+4gPXLI1+y2uazws5TMe8w==", + "dev": true + }, + "node_modules/charset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dev": true, + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "dev": true, + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true }, - "json-logic-js": { + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csv-parse": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", + "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", + "dev": true + }, + "node_modules/culori": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.2.0.tgz", + "integrity": "sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.3.1.tgz", + "integrity": "sha512-dCi91VD+57lkoBd10CjdW4wPOeOPYvvzQbxti6xmyQbDMbCeCXwNq2KdoU798I4OsCcD5B+n7yVG7HAgYW+cvw==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/directory-tree": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/directory-tree/-/directory-tree-3.5.1.tgz", + "integrity": "sha512-HqjZ49fDzUnKYUhHxVw9eKBqbQ+lL0v4kSBInlDlaktmLtGoV9tC54a6A0ZfYeIrkMHWTE6MwwmUXP477+UEKQ==", + "dev": true, + "dependencies": { + "command-line-args": "^5.2.0", + "command-line-usage": "^6.1.1" + }, + "bin": { + "directory-tree": "bin/index.js" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filesize": { + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz", + "integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==", + "dev": true, + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-reasons": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/http-reasons/-/http-reasons-0.1.0.tgz", + "integrity": "sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==", + "dev": true + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/httpntlm": { + "version": "1.8.13", + "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz", + "integrity": "sha512-2F2FDPiWT4rewPzNMg3uPhNkP3NExENlUGADRUDPQvuftuUTGW98nLZtGemCIW3G40VhWZYgkIDcQFAwZ3mf2Q==", + "dev": true, + "funding": [ + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=2CKNJLZJBW8ZC" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/samdecrock" + } + ], + "dependencies": { + "des.js": "^1.0.1", + "httpreq": ">=0.4.22", + "js-md4": "^0.3.2", + "underscore": "~1.12.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/httpreq": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.1.tgz", + "integrity": "sha512-uhSZLPPD2VXXOSN8Cni3kIsoFHaU2pT/nySEU/fHr/ePbqHYr0jeiQRmUKLEirC09SFPsdMoA7LU7UXMd/w0Kw==", + "dev": true, + "engines": { + "node": ">= 6.15.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jose": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", + "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsprim": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.2.tgz", - "integrity": "sha512-ZBtBdMJieqQcH7IX/LaBsr5pX+Y5JIW+EhejtM3Ffg2jdN9Iwf+Ht6TbHnvAZ/YtwyuhPaCBlnvzrwVeWdvGDQ==" + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/liquid-json": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/liquid-json/-/liquid-json-0.3.1.tgz", + "integrity": "sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-format": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mime-format/-/mime-format-2.0.1.tgz", + "integrity": "sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==", + "dev": true, + "dependencies": { + "charset": "^1.0.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/newman": { + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/knutties/newman.git#7bdb3067b77b39a04520411c6cd89fb998a33534", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@postman/tough-cookie": "4.1.3-postman.1", + "async": "3.2.4", + "chardet": "1.6.0", + "cli-progress": "3.12.0", + "cli-table3": "0.6.3", + "colors": "1.4.0", + "commander": "11.0.0", + "csv-parse": "4.16.3", + "directory-tree": "3.5.1", + "eventemitter3": "4.0.7", + "filesize": "10.0.12", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mkdirp": "3.0.1", + "postman-collection": "4.2.1", + "postman-collection-transformer": "4.1.7", + "postman-request": "2.88.1-postman.33", + "postman-runtime": "7.33.0", + "pretty-ms": "7.0.1", + "semver": "7.5.4", + "serialised-error": "1.1.3", + "word-wrap": "1.2.5", + "xmlbuilder": "15.1.1" + }, + "bin": { + "newman": "bin/newman.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/newman/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-oauth1": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-oauth1/-/node-oauth1-1.3.0.tgz", + "integrity": "sha512-0yggixNfrA1KcBwvh/Hy2xAS1Wfs9dcg6TdFf2zN7gilcAigMdrtZ4ybrBSXBgLvGDw9V1p2MRnGBMq7XjTWLg==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", + "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^2.1.1" + }, + "engines": { + "node": ">= 14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postman-collection": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", + "integrity": "sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==", + "dev": true, + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.5.4", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection-transformer": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/postman-collection-transformer/-/postman-collection-transformer-4.1.7.tgz", + "integrity": "sha512-SxJkm/LnlFZs2splUBnS4jQFicgBptghpm4voHtNnaum3Ad64E2MHLV4fJhv58dVUmFwdSwdQUN3m2q0iLecnQ==", + "dev": true, + "dependencies": { + "commander": "8.3.0", + "inherits": "2.0.4", + "lodash": "4.17.21", + "semver": "7.5.4", + "strip-json-comments": "3.1.1" + }, + "bin": { + "postman-collection-transformer": "bin/transform-collection.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-collection-transformer/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/postman-request": { + "version": "2.88.1-postman.33", + "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.33.tgz", + "integrity": "sha512-uL9sCML4gPH6Z4hreDWbeinKU0p0Ke261nU7OvII95NU22HN6Dk7T/SaVPaj6T4TsQqGKIFw6/woLZnH7ugFNA==", + "dev": true, + "dependencies": { + "@postman/form-data": "~3.1.1", + "@postman/tough-cookie": "~4.1.3-postman.1", + "@postman/tunnel-agent": "^0.6.3", + "aws-sign2": "~0.7.0", + "aws4": "^1.12.0", + "brotli": "^1.3.3", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "har-validator": "~5.1.3", + "http-signature": "~1.3.1", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "^2.1.35", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.3", + "safe-buffer": "^5.1.2", + "stream-length": "^1.0.2", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postman-runtime": { + "version": "7.33.0", + "resolved": "https://registry.npmjs.org/postman-runtime/-/postman-runtime-7.33.0.tgz", + "integrity": "sha512-cYCb+5Y12FwZU/T3gOj2SKiOz38pisVLc0tdppb+ZlG7iqn5aLgxghJwhjG62pZCV6uixKiQX1hNdLSk9a9Xtw==", + "dev": true, + "dependencies": { + "@postman/tough-cookie": "4.1.3-postman.1", + "async": "3.2.4", + "aws4": "1.12.0", + "handlebars": "4.7.8", + "httpntlm": "1.8.13", + "jose": "4.14.4", + "js-sha512": "0.8.0", + "lodash": "4.17.21", + "mime-types": "2.1.35", + "node-oauth1": "1.3.0", + "performance-now": "2.1.0", + "postman-collection": "4.2.0", + "postman-request": "2.88.1-postman.33", + "postman-sandbox": "4.2.7", + "postman-url-encoder": "3.0.5", + "serialised-error": "1.1.3", + "strip-json-comments": "3.1.1", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/postman-runtime/node_modules/postman-collection": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.0.tgz", + "integrity": "sha512-tvOLgN1h6Kab6dt43PmBoV5kYO/YUta3x0C2QqfmbzmHZe47VTpZ/+gIkGlbNhjKNPUUub5X6ehxYKoaTYdy1w==", + "dev": true, + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.5.4", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-sandbox": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/postman-sandbox/-/postman-sandbox-4.2.7.tgz", + "integrity": "sha512-/EcCrKnb/o+9iLS4u+H76E0kBomJFjPptVjoDiq1uZ7Es/4aTv0MAX+0aoDxdDO+0h9sl8vy65uKQwyjN7AOaw==", + "dev": true, + "dependencies": { + "lodash": "4.17.21", + "postman-collection": "4.2.0", + "teleport-javascript": "1.0.0", + "uvm": "2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postman-sandbox/node_modules/postman-collection": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.0.tgz", + "integrity": "sha512-tvOLgN1h6Kab6dt43PmBoV5kYO/YUta3x0C2QqfmbzmHZe47VTpZ/+gIkGlbNhjKNPUUub5X6ehxYKoaTYdy1w==", + "dev": true, + "dependencies": { + "@faker-js/faker": "5.5.3", + "file-type": "3.9.0", + "http-reasons": "0.1.0", + "iconv-lite": "0.6.3", + "liquid-json": "0.3.1", + "lodash": "4.17.21", + "mime-format": "2.0.1", + "mime-types": "2.1.35", + "postman-url-encoder": "3.0.5", + "semver": "7.5.4", + "uuid": "8.3.2" + }, + "engines": { + "node": ">=10" + } }, - "typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "node_modules/postman-url-encoder": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz", + "integrity": "sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialised-error": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/serialised-error/-/serialised-error-1.1.3.tgz", + "integrity": "sha512-vybp3GItaR1ZtO2nxZZo8eOo7fnVaNtP3XE2vJKgzkKR2bagCkdJ1EpYYhEMd3qu/80DwQk9KjsNSxE3fXWq0g==", + "dev": true, + "dependencies": { + "object-hash": "^1.1.2", + "stack-trace": "0.0.9", + "uuid": "^3.0.0" + } + }, + "node_modules/serialised-error/node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/serialised-error/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.9.tgz", + "integrity": "sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/stream-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", + "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", + "dev": true, + "dependencies": { + "bluebird": "^2.6.2" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", + "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "7.1.6", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "dev": true, + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/teleport-javascript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/teleport-javascript/-/teleport-javascript-1.0.0.tgz", + "integrity": "sha512-j1llvWVFyEn/6XIFDfX5LAU43DXe0GCt3NfXDwJ8XpRRMkS+i50SAkonAONBy+vxwPFBd50MFU8a2uj8R/ccLg==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvm": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/uvm/-/uvm-2.1.1.tgz", + "integrity": "sha512-BZ5w8adTpNNr+zczOBRpaX/hH8UPKAf7fmCnidrcsqt3bn8KT9bDIfuS7hgRU9RXgiN01su2pwysBONY6w8W5w==", + "dev": true, + "dependencies": { + "flatted": "3.2.6" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "dev": true, + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } } } } diff --git a/package.json b/package.json index 4af52fdc6..5cf3ede2a 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,17 @@ { - "name": "test", + "name": "context-aware-configuration", "version": "0.0.1", - "description": "", - "main": "index.js", + "private": true, + "description": "This is just to run automated newman tests for this service", "scripts": { - "test": "node ./test/index.js", - "compile": "npx tsc" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "json-logic-js": "^2.0.2" + "test": "./node_modules/.bin/newman dir-run ./postman/cac && ./node_modules/.bin/newman dir-run -e postman/experiment-platform-local-env.postman_environment.json ./postman/experimentation-platform ", + "load_exp_tests": "npx newman dir-import postman/experimentation-platform -o postman/experimentation-platform.postman_collection.json", + "load_cac_tests": "npx newman dir-import postman/cac -o postman/cac.postman_collection.json" }, "devDependencies": { - "@types/json-logic-js": "^2.0.1", - "@types/node": "^18.11.9", - "typescript": "^4.9.3" + "axios": "^0.28.0", + "daisyui": "^4.3.1", + "newman": "git+ssh://git@github.com:knutties/newman.git#feature/newman-dir", + "tailwindcss": "^3.3.5" } } diff --git a/postman/cac.postman_collection.json b/postman/cac.postman_collection.json new file mode 100644 index 000000000..72101baa4 --- /dev/null +++ b/postman/cac.postman_collection.json @@ -0,0 +1,851 @@ +{ + "info": { + "_postman_id": "12a7fe9f-2a54-4afa-aa48-4625bfc8e858", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "15880623" + }, + "item": [ + { + "name": "config", + "item": [ + { + "name": "Get Config", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + " let response = pm.response.json();", + " let expected_response = {", + " \"contexts\": [],", + " \"overrides\": {},", + " \"default_configs\": {}", + " };", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response));", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" + ], + "path": [ + "config" + ] + } + }, + "response": [] + }, + { + "name": "resolve", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/config/resolve?clientId=zee5", + "host": [ + "{{host}}" + ], + "path": [ + "config", + "resolve" + ], + "query": [ + { + "key": "clientId", + "value": "zee5" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Default Config", + "item": [ + { + "name": "Add default-config key", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(key, value) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const default_configs = resp_obj.default_configs;", + "", + " console.log(`Checking if key=${key} with value=${value} in default_configs`);", + " pm.expect(default_configs[key]).to.be.eq(value);", + " });", + "}", + "", + "pm.test(\"201 check\", function () {", + " pm.response.to.have.status(201);", + "})", + "", + "pm.test(\"Check if key added to default config\", function () {", + " const key = \"key1\", value = \"value1\";", + " getConfigAndTest(key, value);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"value\":\"value1\",\"schema\":{\"type\":\"string\",\"pattern\":\".*\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Dimension", + "item": [ + { + "name": "Create Dimension", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"201 Check\", function () {", + " pm.response.to.have.status(201);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"dimension\":\"clientId\",\"priority\":100,\"schema\":{\"type\":\"string\",\"pattern\":\"^[a-z0-9].*$\"}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Context", + "item": [ + { + "name": "Create Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value2\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Update Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + " ", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id]; ", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"piyaz\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"piyaz\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } + }, + "response": [] + }, + { + "name": "Move Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "", + "function getConfigAndTest(context_id, override_id, expected_condition, expected_override) {", + " const getRequest = {", + " url: `${host}/config`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const resp_obj = response.json();", + " const contexts = resp_obj.contexts;", + " const overrides = resp_obj.overrides;", + "", + " console.log(`Checking if context=${context_id} contexts list.`);", + " const available_context_ids = contexts.map((context) => context.id);", + " pm.expect(available_context_ids).to.include(context_id);", + " if (pm.environment.get(\"old_context_id\") in available_context_ids) {", + " throw \"old context not removed on move\"", + " }", + "", + " const context = contexts.find((context) => context.id === context_id);", + "", + " console.log(`Checking if context condition matches.`);", + " const context_condition = context.condition;", + " console.log(`Expected => ${JSON.stringify(expected_condition)}`);", + " console.log(`Actual => ${JSON.stringify(context_condition)}`);", + " pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition));", + "", + " console.log(`Checking if context=${context_id} uses override=${override_id}`);", + " const context_override_ids = context.override_with_keys;", + " pm.expect(context_override_ids).to.include(override_id);", + "", + "", + " console.log(`Checking override=${override_id} in overrides object`);", + " const override = overrides[override_id];", + " console.log(`Expected => ${JSON.stringify(expected_override)}`);", + " console.log(`Actual => ${JSON.stringify(override)}`);", + " pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override));", + " });", + "}", + "", + "pm.test(\"200 check\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " pm.environment.set(\"old_context_id\", pm.environment.get(\"context_id\"));", + " pm.environment.set(\"old_override_id\", pm.environment.get(\"override_id\"));", + " pm.environment.set(\"context_id\", context_id);", + " pm.environment.set(\"override_id\", override_id);", + "", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Check if context is added\", function () {", + " const response = pm.response.json();", + " const context_id = response.context_id;", + " const override_id = response.override_id;", + "", + " const condition = {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " };", + " const override = {", + " \"key1\": \"value2\"", + " };", + "", + "", + " getConfigAndTest(context_id, override_id, condition, override);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\"override\":{\"key1\":\"value3\"},\"context\":{\"==\":[{\"var\":\"clientId\"},\"tamatar\"]}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Get Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const expected_context = {", + " \"id\": pm.environment.get(\"context_id\"),", + " \"value\": {", + " \"==\": [", + " {", + " \"var\": \"clientId\"", + " },", + " \"tamatar\"", + " ]", + " },", + " \"override_id\": pm.environment.get(\"override_id\"),", + " \"priority\": 100,", + " \"override\": {", + " \"key1\": \"value3\"", + " }", + "};", + "", + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "pm.test(\"Context equality check\", function() {", + " const response = pm.response.json();", + " ", + " delete response.created_at;", + " delete response.created_by;", + "", + " pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context));", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + }, + { + "name": "List Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})", + "", + "", + "pm.test(\"Response validation\", function() {", + " const response = pm.response.json();", + " if (response.length == 0) {", + " throw \"list context should return at least one context now\"", + " }", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "list" + ] + } + }, + "response": [] + }, + { + "name": "Delete Context", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.variables.get(\"host\");", + "const context_id = pm.environment.get(\"context_id\");", + "", + "pm.test(\"204 check\", function() {", + " pm.response.to.have.status(204);", + "})", + "", + "pm.test(\"Fetch for context should fail with 404\", function () {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " console.log(`alloo ${error}`);", + " throw error;", + " }", + "", + " pm.expect(response.code).to.be.eq(404);", + " });", + "})", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "audit log", + "item": [ + { + "name": "get_logs", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('expect response be 200', function () {", + " pm.response.to.be.ok;", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/audit", + "host": [ + "{{host}}" + ], + "path": [ + "audit" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} \ No newline at end of file diff --git a/postman/cac/.event.meta.json b/postman/cac/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/cac/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/cac/.info.json b/postman/cac/.info.json new file mode 100644 index 000000000..59d37a94c --- /dev/null +++ b/postman/cac/.info.json @@ -0,0 +1,8 @@ +{ + "info": { + "_postman_id": "12a7fe9f-2a54-4afa-aa48-4625bfc8e858", + "name": "cac", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "15880623" + } +} diff --git a/postman/cac/.meta.json b/postman/cac/.meta.json new file mode 100644 index 000000000..7d010895f --- /dev/null +++ b/postman/cac/.meta.json @@ -0,0 +1,9 @@ +{ + "childrenOrder": [ + "config", + "Default Config", + "Dimension", + "Context", + "audit log" + ] +} diff --git a/postman/cac/.variable.json b/postman/cac/.variable.json new file mode 100644 index 000000000..5460e39fb --- /dev/null +++ b/postman/cac/.variable.json @@ -0,0 +1,14 @@ +{ + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/cac/Context/.meta.json b/postman/cac/Context/.meta.json new file mode 100644 index 000000000..e947b4385 --- /dev/null +++ b/postman/cac/Context/.meta.json @@ -0,0 +1,10 @@ +{ + "childrenOrder": [ + "Create Context", + "Update Context", + "Move Context", + "Get Context", + "List Context", + "Delete Context" + ] +} diff --git a/postman/cac/Context/Create Context/.event.meta.json b/postman/cac/Context/Create Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Create Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Create Context/event.test.js b/postman/cac/Context/Create Context/event.test.js new file mode 100644 index 000000000..1bd87dec4 --- /dev/null +++ b/postman/cac/Context/Create Context/event.test.js @@ -0,0 +1,78 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); \ No newline at end of file diff --git a/postman/cac/Context/Create Context/request.json b/postman/cac/Context/Create Context/request.json new file mode 100644 index 000000000..6bf6048f0 --- /dev/null +++ b/postman/cac/Context/Create Context/request.json @@ -0,0 +1,50 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "override": { + "key1": "value2" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/Create Context/response.json b/postman/cac/Context/Create Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Create Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Delete Context/.event.meta.json b/postman/cac/Context/Delete Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Delete Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Delete Context/event.test.js b/postman/cac/Context/Delete Context/event.test.js new file mode 100644 index 000000000..88a601ebc --- /dev/null +++ b/postman/cac/Context/Delete Context/event.test.js @@ -0,0 +1,27 @@ +const host = pm.variables.get("host"); +const context_id = pm.environment.get("context_id"); + +pm.test("204 check", function() { + pm.response.to.have.status(204); +}) + +pm.test("Fetch for context should fail with 404", function () { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + console.log(`alloo ${error}`); + throw error; + } + + pm.expect(response.code).to.be.eq(404); + }); +}) \ No newline at end of file diff --git a/postman/cac/Context/Delete Context/request.json b/postman/cac/Context/Delete Context/request.json new file mode 100644 index 000000000..ed19fe4c4 --- /dev/null +++ b/postman/cac/Context/Delete Context/request.json @@ -0,0 +1,25 @@ +{ + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/Delete Context/response.json b/postman/cac/Context/Delete Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Delete Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Get Context/.event.meta.json b/postman/cac/Context/Get Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Get Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Get Context/event.test.js b/postman/cac/Context/Get Context/event.test.js new file mode 100644 index 000000000..5ccf43d08 --- /dev/null +++ b/postman/cac/Context/Get Context/event.test.js @@ -0,0 +1,29 @@ +const expected_context = { + "id": pm.environment.get("context_id"), + "value": { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + }, + "override_id": pm.environment.get("override_id"), + "priority": 100, + "override": { + "key1": "value3" + } +}; + +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) + +pm.test("Context equality check", function() { + const response = pm.response.json(); + + delete response.created_at; + delete response.created_by; + + pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_context)); +}); diff --git a/postman/cac/Context/Get Context/request.json b/postman/cac/Context/Get Context/request.json new file mode 100644 index 000000000..948e54cd4 --- /dev/null +++ b/postman/cac/Context/Get Context/request.json @@ -0,0 +1,25 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "{{context_id}}" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/Get Context/response.json b/postman/cac/Context/Get Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Get Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/List Context/.event.meta.json b/postman/cac/Context/List Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/List Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/List Context/event.test.js b/postman/cac/Context/List Context/event.test.js new file mode 100644 index 000000000..9abf929a8 --- /dev/null +++ b/postman/cac/Context/List Context/event.test.js @@ -0,0 +1,11 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) + + +pm.test("Response validation", function() { + const response = pm.response.json(); + if (response.length == 0) { + throw "list context should return at least one context now" + } +}); diff --git a/postman/cac/Context/List Context/request.json b/postman/cac/Context/List Context/request.json new file mode 100644 index 000000000..78c1e260f --- /dev/null +++ b/postman/cac/Context/List Context/request.json @@ -0,0 +1,25 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/context/list", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "list" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/List Context/response.json b/postman/cac/Context/List Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/List Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Move Context/.event.meta.json b/postman/cac/Context/Move Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Move Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Move Context/event.test.js b/postman/cac/Context/Move Context/event.test.js new file mode 100644 index 000000000..3f35669d1 --- /dev/null +++ b/postman/cac/Context/Move Context/event.test.js @@ -0,0 +1,83 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + if (pm.environment.get("old_context_id") in available_context_ids) { + throw "old context not removed on move" + } + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("old_context_id", pm.environment.get("context_id")); + pm.environment.set("old_override_id", pm.environment.get("override_id")); + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); \ No newline at end of file diff --git a/postman/cac/Context/Move Context/request.json b/postman/cac/Context/Move Context/request.json new file mode 100644 index 000000000..c19dadefb --- /dev/null +++ b/postman/cac/Context/Move Context/request.json @@ -0,0 +1,52 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "override": { + "key1": "value3" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "tamatar" + ] + } + } + }, + "url": { + "raw": "{{host}}/context/move/{{context_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "context", + "move", + "{{context_id}}" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/Move Context/response.json b/postman/cac/Context/Move Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Move Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Context/Update Context/.event.meta.json b/postman/cac/Context/Update Context/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Context/Update Context/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Context/Update Context/event.test.js b/postman/cac/Context/Update Context/event.test.js new file mode 100644 index 000000000..1bd87dec4 --- /dev/null +++ b/postman/cac/Context/Update Context/event.test.js @@ -0,0 +1,78 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(context_id, override_id, expected_condition, expected_override) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const contexts = resp_obj.contexts; + const overrides = resp_obj.overrides; + + console.log(`Checking if context=${context_id} contexts list.`); + const available_context_ids = contexts.map((context) => context.id); + pm.expect(available_context_ids).to.include(context_id); + + const context = contexts.find((context) => context.id === context_id); + + console.log(`Checking if context condition matches.`); + const context_condition = context.condition; + console.log(`Expected => ${JSON.stringify(expected_condition)}`); + console.log(`Actual => ${JSON.stringify(context_condition)}`); + pm.expect(JSON.stringify(context_condition)).to.be.eq(JSON.stringify(expected_condition)); + + console.log(`Checking if context=${context_id} uses override=${override_id}`); + const context_override_ids = context.override_with_keys; + pm.expect(context_override_ids).to.include(override_id); + + + console.log(`Checking override=${override_id} in overrides object`); + const override = overrides[override_id]; + console.log(`Expected => ${JSON.stringify(expected_override)}`); + console.log(`Actual => ${JSON.stringify(override)}`); + pm.expect(JSON.stringify(expected_override)).to.be.eq(JSON.stringify(override)); + }); +} + +pm.test("200 check", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + pm.environment.set("context_id", context_id); + pm.environment.set("override_id", override_id); + + pm.response.to.have.status(200); +}) + +pm.test("Check if context is added", function () { + const response = pm.response.json(); + const context_id = response.context_id; + const override_id = response.override_id; + + const condition = { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + }; + const override = { + "key1": "value2" + }; + + + getConfigAndTest(context_id, override_id, condition, override); +}); \ No newline at end of file diff --git a/postman/cac/Context/Update Context/request.json b/postman/cac/Context/Update Context/request.json new file mode 100644 index 000000000..8d1043f78 --- /dev/null +++ b/postman/cac/Context/Update Context/request.json @@ -0,0 +1,50 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "override": { + "key1": "value3" + }, + "context": { + "==": [ + { + "var": "clientId" + }, + "piyaz" + ] + } + } + }, + "url": { + "raw": "{{host}}/context", + "host": [ + "{{host}}" + ], + "path": [ + "context" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Context/Update Context/response.json b/postman/cac/Context/Update Context/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Context/Update Context/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Default Config/.meta.json b/postman/cac/Default Config/.meta.json new file mode 100644 index 000000000..1ac6ce34b --- /dev/null +++ b/postman/cac/Default Config/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "Add default-config key" + ] +} diff --git a/postman/cac/Default Config/Add default-config key/.event.meta.json b/postman/cac/Default Config/Add default-config key/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Default Config/Add default-config key/event.test.js b/postman/cac/Default Config/Add default-config key/event.test.js new file mode 100644 index 000000000..28b30c2c3 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/event.test.js @@ -0,0 +1,34 @@ +const host = pm.variables.get("host"); + +function getConfigAndTest(key, value) { + const getRequest = { + url: `${host}/config`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const resp_obj = response.json(); + const default_configs = resp_obj.default_configs; + + console.log(`Checking if key=${key} with value=${value} in default_configs`); + pm.expect(default_configs[key]).to.be.eq(value); + }); +} + +pm.test("201 check", function () { + pm.response.to.have.status(200); +}) + +pm.test("Check if key added to default config", function () { + const key = "key1", value = "value1"; + getConfigAndTest(key, value); +}); \ No newline at end of file diff --git a/postman/cac/Default Config/Add default-config key/request.json b/postman/cac/Default Config/Add default-config key/request.json new file mode 100644 index 000000000..c5345661f --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/request.json @@ -0,0 +1,45 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + } + }, + "url": { + "raw": "{{host}}/default-config/key1", + "host": [ + "{{host}}" + ], + "path": [ + "default-config", + "key1" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Default Config/Add default-config key/response.json b/postman/cac/Default Config/Add default-config key/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Default Config/Add default-config key/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/Dimension/.meta.json b/postman/cac/Dimension/.meta.json new file mode 100644 index 000000000..bc8f8215b --- /dev/null +++ b/postman/cac/Dimension/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "Create Dimension" + ] +} diff --git a/postman/cac/Dimension/Create Dimension/.event.meta.json b/postman/cac/Dimension/Create Dimension/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/Dimension/Create Dimension/event.test.js b/postman/cac/Dimension/Create Dimension/event.test.js new file mode 100644 index 000000000..dfa01aa5c --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/event.test.js @@ -0,0 +1,3 @@ +pm.test("201 Check", function () { + pm.response.to.have.status(201); +}) \ No newline at end of file diff --git a/postman/cac/Dimension/Create Dimension/request.json b/postman/cac/Dimension/Create Dimension/request.json new file mode 100644 index 000000000..b9b4696da --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/request.json @@ -0,0 +1,45 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "dimension": "clientId", + "priority": 100, + "schema": { + "type": "string", + "pattern": "^[a-z0-9].*$" + } + } + }, + "url": { + "raw": "{{host}}/dimension", + "host": [ + "{{host}}" + ], + "path": [ + "dimension" + ] + } +} \ No newline at end of file diff --git a/postman/cac/Dimension/Create Dimension/response.json b/postman/cac/Dimension/Create Dimension/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/Dimension/Create Dimension/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/audit log/.meta.json b/postman/cac/audit log/.meta.json new file mode 100644 index 000000000..6d5cafaff --- /dev/null +++ b/postman/cac/audit log/.meta.json @@ -0,0 +1,5 @@ +{ + "childrenOrder": [ + "get_logs" + ] +} diff --git a/postman/cac/audit log/get_logs/.event.meta.json b/postman/cac/audit log/get_logs/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/audit log/get_logs/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/audit log/get_logs/event.test.js b/postman/cac/audit log/get_logs/event.test.js new file mode 100644 index 000000000..f0145ea58 --- /dev/null +++ b/postman/cac/audit log/get_logs/event.test.js @@ -0,0 +1,3 @@ +pm.test('expect response be 200', function () { + pm.response.to.be.ok; +}); diff --git a/postman/cac/audit log/get_logs/request.json b/postman/cac/audit log/get_logs/request.json new file mode 100644 index 000000000..3052131db --- /dev/null +++ b/postman/cac/audit log/get_logs/request.json @@ -0,0 +1,19 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/audit", + "host": [ + "{{host}}" + ], + "path": [ + "audit" + ] + } +} \ No newline at end of file diff --git a/postman/cac/audit log/get_logs/response.json b/postman/cac/audit log/get_logs/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/audit log/get_logs/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/config/.meta.json b/postman/cac/config/.meta.json new file mode 100644 index 000000000..97068a209 --- /dev/null +++ b/postman/cac/config/.meta.json @@ -0,0 +1,6 @@ +{ + "childrenOrder": [ + "Get Config", + "resolve" + ] +} diff --git a/postman/cac/config/Get Config/.event.meta.json b/postman/cac/config/Get Config/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/cac/config/Get Config/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/cac/config/Get Config/event.test.js b/postman/cac/config/Get Config/event.test.js new file mode 100644 index 000000000..bb89bec7a --- /dev/null +++ b/postman/cac/config/Get Config/event.test.js @@ -0,0 +1,10 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); + let response = pm.response.json(); + let expected_response = { + "contexts": [], + "overrides": {}, + "default_configs": {} + }; + pm.expect(JSON.stringify(response)).to.be.eq(JSON.stringify(expected_response)); +}) diff --git a/postman/cac/config/Get Config/request.json b/postman/cac/config/Get Config/request.json new file mode 100644 index 000000000..bc973fd23 --- /dev/null +++ b/postman/cac/config/Get Config/request.json @@ -0,0 +1,24 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/config", + "host": [ + "{{host}}" + ], + "path": [ + "config" + ] + } +} \ No newline at end of file diff --git a/postman/cac/config/Get Config/response.json b/postman/cac/config/Get Config/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/config/Get Config/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/config/resolve/request.json b/postman/cac/config/resolve/request.json new file mode 100644 index 000000000..fc4ab4343 --- /dev/null +++ b/postman/cac/config/resolve/request.json @@ -0,0 +1,26 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/config/resolve?clientId=zee5", + "host": [ + "{{host}}" + ], + "path": [ + "config", + "resolve" + ], + "query": [ + { + "key": "clientId", + "value": "zee5" + } + ] + } +} \ No newline at end of file diff --git a/postman/cac/config/resolve/response.json b/postman/cac/config/resolve/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/cac/config/resolve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/cac/event.prerequest.js b/postman/cac/event.prerequest.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/cac/event.test.js b/postman/cac/event.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/experiment-platform-local-env.postman_environment.json b/postman/experiment-platform-local-env.postman_environment.json new file mode 100644 index 000000000..657343757 --- /dev/null +++ b/postman/experiment-platform-local-env.postman_environment.json @@ -0,0 +1,21 @@ +{ + "id": "70711214-9fb2-4d08-bd31-09c95e66dec7", + "name": "experiment-platform-local-env", + "values": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default", + "enabled": true + }, + { + "key": "token", + "value": "12345678", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2023-08-09T06:50:28.154Z", + "_postman_exported_using": "Postman/10.14.2" +} diff --git a/postman/experimentation-platform.postman_collection.json b/postman/experimentation-platform.postman_collection.json new file mode 100644 index 000000000..a82e81b5c --- /dev/null +++ b/postman/experimentation-platform.postman_collection.json @@ -0,0 +1,1458 @@ +{ + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "item": [ + { + "name": "Create Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value1-control\",", + " \"pmTestKey2\": \"value1-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1\": \"value2-test\",", + " \"pmTestKey2\": \"value2-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac1\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey1`,", + " `pmTestKey2`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, schema: { type: \"string\", enum: [\"android\", \"ios\", \"web\"] }},", + " {name: \"client\", priority: 100, schema: { type: \"string\", pattern: \".*\" }},", + " {name: \"variantIds\", priority: 1000, schema: { type: \"string\", pattern: \".*\" }}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"schema\": dimension.schema", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_default_config_keys();", + "create_dimensions();" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"name\":\"experiment-1\",\"override_keys\":[\"pmTestKey1\",\"pmTestKey2\"],\"traffic_percentage\":10,\"context\":{\"and\":[{\"==\":[{\"var\":\"os\"},\"ios\"]},{\"==\":[{\"var\":\"client\"},\"testClientCac1\"]}]},\"variants\":[{\"id\":\"control\",\"variant_type\":\"CONTROL\",\"overrides\":{\"pmTestKey1\":\"value1-control\",\"pmTestKey2\":\"value1-control\"}},{\"id\":\"test1\",\"variant_type\":\"EXPERIMENTAL\",\"overrides\":{\"pmTestKey1\":\"value2-test\",\"pmTestKey2\":\"value2-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Get Experiment", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}" + ] + } + }, + "response": [] + }, + { + "name": "Ramp", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + " console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`);", + " pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage);", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test traffic percentage\", function() {", + " const experiment_id = pm.environment.get(\"experiment_id\");", + "", + " fetch_experiment_n_test(experiment_id, 46);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"traffic_percentage\":46}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/ramp", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "ramp" + ] + } + }, + "response": [] + }, + { + "name": "Conclude", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "const experiment_id = pm.environment.get(\"experiment_id\");", + "", + "function fetch_config_n_test(variants, winner_variant_id) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/config`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'Contet-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch config\");", + " throw error;", + " }", + "", + " const config = response.json();", + " const contexts = config.contexts;", + " const overrides = config.overrides;", + "", + " const winner_variant = variants.find(variant => variant.id === winner_variant_id);", + " const winner_variant_override_id = winner_variant.override_id;", + " ", + " // there should be only one context with the winner variant override id", + " const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id));", + " console.log(\"Context with winner variant override\");", + " console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4));", + " pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1);", + "", + " // there should be 0 contexts with variant as a dimension", + " const contexts_with_variant_dim = contexts", + " .filter(", + " (context) => ", + " context.condition.and", + " ?.map(", + " (condition) => ", + " Object.keys(condition)", + " .map((k) => condition[k][0].var === \"variant\")", + " .reduce((p, c) => p || c, false))", + " .reduce((p, c) => p || c, false)", + " );", + " pm.expect(contexts_with_variant_dim.length).to.be.eq(0);", + "", + " // checking if winner override exists and is same as the expected override", + " const winner_variant_context = contexts_with_winner_variant_override[0]; ", + " pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1);", + " pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id));", + "", + " // checking if all the discarded overrides are removed", + " const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id);", + " const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id);", + " const available_overrides = Object.keys(overrides);", + " for(const ao of available_overrides) {", + " pm.expect(discarded_variants_override_ids).to.not.include(ao);", + " }", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const status = experiment.status;", + " pm.expect(status).to.be.eq(expected_status);", + "", + " const variants = experiment.variants;", + " fetch_config_n_test(variants, winner_variant_id);", + " });", + "}", + "", + "pm.test(\"200 OK\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Conclude correctness\", function() {", + " const winner_variant_id = `${experiment_id}-control`;", + " fetch_experiment_n_test(experiment_id, winner_variant_id, \"CONCLUDED\")", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"chosen_variant\":\"{{experiment_id}}-control\"}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/conclude", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "conclude" + ] + } + }, + "response": [] + }, + { + "name": "Create Experiment 2", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey4`,", + " `pmTestKey3`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "function create_dimensions() {", + " const dimensions = [", + " {name: \"os\", priority: 10, type: \"STRING\"},", + " {name: \"client\", priority: 100, type: \"STRING\"},", + " {name: \"variantIds\", priority: 1000, type: \"STRING\"}", + " ];", + "", + " for (const dimension of dimensions) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/dimension`,", + " 'header': {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"dimension\": dimension.name,", + " \"priority\": dimension.priority,", + " \"type\": dimension.type", + " })", + " }", + " };", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating dimension: ${dimension.name}`);", + " console.log(error);", + " return;", + " }", + " console.log(`Created dimension: ${dimension.name}`);", + " });", + " }", + "}", + "", + "create_dimensions();", + "create_default_config_keys();" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const context = experiment.context;", + " console.log(\"Testing Context of Experiment\");", + " console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`);", + " console.log(`Actual: ${JSON.stringify(context, null, 2)}`);", + " pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context));", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test created contexts\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + "", + "", + " const expected_context = {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " }", + " ]", + " };", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value3-control\",", + " \"pmTestKey4\": \"value3-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey3\": \"value4-test\",", + " \"pmTestKey4\": \"value4-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "mjos", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"name\":\"experiment-1\",\"override_keys\":[\"pmTestKey3\",\"pmTestKey4\"],\"traffic_percentage\":10,\"context\":{\"and\":[{\"==\":[{\"var\":\"os\"},\"ios\"]},{\"==\":[{\"var\":\"client\"},\"testClientCac02\"]}]},\"variants\":[{\"id\":\"control\",\"variant_type\":\"CONTROL\",\"overrides\":{\"pmTestKey3\":\"value3-control\",\"pmTestKey4\":\"value3-control\"}},{\"id\":\"test1\",\"variant_type\":\"EXPERIMENTAL\",\"overrides\":{\"pmTestKey3\":\"value4-test\",\"pmTestKey4\":\"value4-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } + }, + "response": [] + }, + { + "name": "Update Override Keys", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "function create_default_config_keys() {", + " let keys = [", + " `pmTestKey1972`,", + " `pmTestKey1999`", + " ];", + "", + " for (const key of keys) {", + " const options = {", + " 'method': 'PUT',", + " 'url': `${host}/default-config/${key}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'x-tenant': 'mjos',", + " 'Content-Type': 'application/json'", + " },", + " \"body\": {", + " \"mode\": \"raw\",", + " \"raw\": JSON.stringify({", + " \"value\": \"value1\",", + " \"schema\": {", + " \"type\": \"string\",", + " \"pattern\": \".*\"", + " }", + " })", + " }", + " };", + " console.log(options);", + " pm.sendRequest(options, function (error, response) {", + " if (error) {", + " console.log(`Error creating default-config key: ${key}`);", + " console.log(error);", + " return;", + " };", + " console.log(`Created default-config key: ${key}`);", + " });", + " }", + "}", + "", + "create_default_config_keys()" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "const host = pm.environment.get(\"host\");", + "const token = pm.environment.get(\"token\");", + "", + "", + "function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) {", + " const getRequest = {", + " url: `${host}/context/${context_id}`,", + " method: 'GET',", + " header: {", + " 'Content-Type': 'application/json',", + " }", + " };", + "", + " ", + " pm.sendRequest(getRequest, (error, response) => {", + " if(error) {", + " console.log(\"Failed to fetch context\");", + " throw error;", + " }", + "", + " const context = response.json();", + "", + " /*********** checking contexts created in CAC **********/;", + " ", + "", + " const variant_override_id = context.override_id;", + " const varaint_context = context.value;", + " const variant_override = context.override;", + "", + " console.log(\"Testing variant override id\");", + " console.log(\"Override from CAC: \\n\", variant_override_id);", + " console.log(\"Expected Context: \\n\", expected_override_id);", + " pm.expect(variant_override_id).to.be.eq(expected_override_id);", + "", + " console.log(\"Testing variant override\");", + " console.log(\"Override from CAC: \\n\", JSON.stringify(variant_override, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_override, null, 2));", + " pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override));", + "", + " console.log(\"Testing variant context\");", + " console.log(\"Context from CAC: \\n\", JSON.stringify(varaint_context, null, 2));", + " console.log(\"Expected Context: \\n\", JSON.stringify(expected_variant_context, null, 2));", + " pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context));", + " });", + "}", + "", + "function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) {", + " const options = {", + " 'method': 'GET',", + " 'url': `${host}/experiments/${experiment_id}`,", + " \"header\": {", + " 'Authorization': `Bearer ${token}`,", + " 'Content-Type': 'application/json'", + " }", + " };", + "", + " pm.sendRequest(options, function(error, response) {", + " if(error) {", + " console.log(\"Failed to fetch experiment\");", + " throw error;", + " }", + " ", + " const experiment = response.json();", + "", + " const variants = experiment.variants;", + " for(const variant of variants) {", + " const variant_id = variant.id;", + "", + " console.log(`TESTING variant: ${variant_id}`);", + "", + " // check if the variant present in the expected_variants", + " const variant_cpy = JSON.parse(JSON.stringify(variant));", + " delete variant_cpy.override_id;", + " delete variant_cpy.context_id;", + "", + " const expected_variant = expected_varaints.find((ev) => ev.id === variant_id);", + " console.log(\"Actual Variant:\", JSON.stringify(variant_cpy, null, 4));", + " console.log(\"Expected Variant:\", JSON.stringify(expected_variant, null, 4));", + " pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant));", + "", + " /*********/", + "", + " const expected_context_id = variant.context_id;", + " const expected_override_id = variant.override_id;", + " const expected_override = variant.overrides;", + " const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context;", + " ", + " fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context);", + " }", + " });", + "}", + "", + "// check experiment creation in experiment", + "pm.test(\"200 OK\", function () {", + " const response = pm.response.json();", + " const experiment_id = response.experiment_id;", + " ", + " pm.environment.set(\"experiment_id\", experiment_id);", + " pm.response.to.have.status(200);", + "});", + "", + "", + "// check for contexts in CAC", + "pm.test(\"Test updated experiment\", function() {", + " const response = pm.response.json();", + " const experiment_id = response.id;", + "", + " const expected_varaints = [", + " {", + " \"id\": `${experiment_id}-control`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7910-an-control\",", + " \"pmTestKey1999\": \"value-6910-an-control\"", + " },", + " \"variant_type\": \"CONTROL\"", + " },", + " {", + " \"id\": `${experiment_id}-test1`,", + " \"overrides\": {", + " \"pmTestKey1972\": \"value-7920-an-test\",", + " \"pmTestKey1999\": \"value-6930-an-test\"", + " },", + " \"variant_type\": \"EXPERIMENTAL\"", + " }", + " ];", + " const expected_variant_contexts = [", + " {", + " \"vid\": `${experiment_id}-control`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-control`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " },", + " {", + " \"vid\": `${experiment_id}-test1`,", + " \"context\": {", + " \"and\": [", + " {", + " \"==\": [", + " {", + " \"var\": \"os\"", + " },", + " \"ios\"", + " ]", + " },", + " {", + " \"==\": [", + " {", + " \"var\": \"client\"", + " },", + " \"testClientCac02\"", + " ]", + " },", + " {", + " \"in\": [", + " `${experiment_id}-test1`,", + " {", + " \"var\": \"variantIds\"", + " }", + " ]", + " }", + " ]", + " }", + " }", + " ];", + "", + " fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"override_keys\":[\"pmTestKey1972\",\"pmTestKey1999\"],\"variants\":[{\"id\":\"{{experiment_id}}-control\",\"overrides\":{\"pmTestKey1972\":\"value-7910-an-control\",\"pmTestKey1999\":\"value-6910-an-control\"}},{\"id\":\"{{experiment_id}}-test1\",\"overrides\":{\"pmTestKey1972\":\"value-7920-an-test\",\"pmTestKey1999\":\"value-6930-an-test\"}}]}" + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/overrides", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "overrides" + ] + } + }, + "response": [] + }, + { + "name": "List experiments [No If-Modified-Since]", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "from_date", + "value": "2023-01-01%2000%3A00%3A00%20UTC" + }, + { + "key": "to_date", + "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"200 check\", function() {", + " pm.response.to.have.status(200);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Thu, 01 Jan 1970 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS,CONCLUDED" + } + ] + } + }, + "response": [] + }, + { + "name": "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"304 check\", function() {", + " pm.response.to.have.status(304);", + "})" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Wed, 01 Jan 2070 00:00:00 +0000", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } + }, + "response": [] + } + ], + "info": { + "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "name": "experimentation-platform", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/experimentation-platform/.event.meta.json b/postman/experimentation-platform/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/.info.json b/postman/experimentation-platform/.info.json new file mode 100644 index 000000000..1d7a38f54 --- /dev/null +++ b/postman/experimentation-platform/.info.json @@ -0,0 +1,7 @@ +{ + "info": { + "_postman_id": "d7e3355b-8480-43d9-87a2-9bbfc158f267", + "name": "experimentation-platform", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + } +} diff --git a/postman/experimentation-platform/.meta.json b/postman/experimentation-platform/.meta.json new file mode 100644 index 000000000..77d3316e1 --- /dev/null +++ b/postman/experimentation-platform/.meta.json @@ -0,0 +1,13 @@ +{ + "childrenOrder": [ + "Create Experiment", + "Get Experiment", + "Ramp", + "Conclude", + "Create Experiment 2", + "Update Override Keys", + "List experiments [No If-Modified-Since]", + "List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]", + "List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]" + ] +} diff --git a/postman/experimentation-platform/.variable.json b/postman/experimentation-platform/.variable.json new file mode 100644 index 000000000..5460e39fb --- /dev/null +++ b/postman/experimentation-platform/.variable.json @@ -0,0 +1,14 @@ +{ + "variable": [ + { + "key": "host", + "value": "http://localhost:8080", + "type": "default" + }, + { + "key": "token", + "value": "12345678", + "type": "default" + } + ] +} diff --git a/postman/experimentation-platform/Conclude/.event.meta.json b/postman/experimentation-platform/Conclude/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Conclude/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Conclude/event.test.js b/postman/experimentation-platform/Conclude/event.test.js new file mode 100644 index 000000000..a59c78306 --- /dev/null +++ b/postman/experimentation-platform/Conclude/event.test.js @@ -0,0 +1,99 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +const experiment_id = pm.environment.get("experiment_id"); + +function fetch_config_n_test(variants, winner_variant_id) { + const options = { + 'method': 'GET', + 'url': `${host}/config`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'Contet-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch config"); + throw error; + } + + const config = response.json(); + const contexts = config.contexts; + const overrides = config.overrides; + + const winner_variant = variants.find(variant => variant.id === winner_variant_id); + const winner_variant_override_id = winner_variant.override_id; + + // there should be only one context with the winner variant override id + const contexts_with_winner_variant_override = contexts.filter((context) => context.override_with_keys.includes(winner_variant_override_id)); + console.log("Context with winner variant override"); + console.log(JSON.stringify(contexts_with_winner_variant_override, null, 4)); + pm.expect(contexts_with_winner_variant_override.length).to.be.eq(1); + + // there should be 0 contexts with variant as a dimension + const contexts_with_variant_dim = contexts + .filter( + (context) => + context.condition.and + ?.map( + (condition) => + Object.keys(condition) + .map((k) => condition[k][0].var === "variant") + .reduce((p, c) => p || c, false)) + .reduce((p, c) => p || c, false) + ); + pm.expect(contexts_with_variant_dim.length).to.be.eq(0); + + // checking if winner override exists and is same as the expected override + const winner_variant_context = contexts_with_winner_variant_override[0]; + pm.expect(winner_variant_context.override_with_keys.length).to.be.eq(1); + pm.expect(JSON.stringify(winner_variant_context.override_with_keys[0])).to.be.eq(JSON.stringify(winner_variant_override_id)); + + // checking if all the discarded overrides are removed + const discarded_variants = variants.filter(variant => variant.id !== winner_variant_id); + const discarded_variants_override_ids = discarded_variants.map(dv => dv.override_id); + const available_overrides = Object.keys(overrides); + for(const ao of available_overrides) { + pm.expect(discarded_variants_override_ids).to.not.include(ao); + } + }); +} + +function fetch_experiment_n_test(experiment_id, winner_variant_id, expected_status) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const status = experiment.status; + pm.expect(status).to.be.eq(expected_status); + + const variants = experiment.variants; + fetch_config_n_test(variants, winner_variant_id); + }); +} + +pm.test("200 OK", function() { + pm.response.to.have.status(200); +}); + +pm.test("Conclude correctness", function() { + const winner_variant_id = `${experiment_id}-control`; + fetch_experiment_n_test(experiment_id, winner_variant_id, "CONCLUDED") +}) \ No newline at end of file diff --git a/postman/experimentation-platform/Conclude/request.json b/postman/experimentation-platform/Conclude/request.json new file mode 100644 index 000000000..af51941aa --- /dev/null +++ b/postman/experimentation-platform/Conclude/request.json @@ -0,0 +1,42 @@ +{ + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "chosen_variant": "{{experiment_id}}-control" + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/conclude", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "conclude" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Conclude/response.json b/postman/experimentation-platform/Conclude/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Conclude/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Create Experiment 2/.event.meta.json b/postman/experimentation-platform/Create Experiment 2/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Create Experiment 2/event.prerequest.js b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js new file mode 100644 index 000000000..7433c1cea --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/event.prerequest.js @@ -0,0 +1,79 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey4`, + `pmTestKey3` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'x-tenant': 'test', + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +function create_dimensions() { + const dimensions = [ + {name: "os", priority: 10, type: "STRING"}, + {name: "client", priority: 100, type: "STRING"}, + {name: "variantIds", priority: 1000, type: "STRING"} + ]; + + for (const dimension of dimensions) { + const options = { + 'method': 'PUT', + 'url': `${host}/dimension`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'x-tenant': 'test', + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "dimension": dimension.name, + "priority": dimension.priority, + "type": dimension.type + }) + } + }; + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating dimension: ${dimension.name}`); + console.log(error); + return; + } + console.log(`Created dimension: ${dimension.name}`); + }); + } +} + +create_dimensions(); +create_default_config_keys(); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/event.test.js b/postman/experimentation-platform/Create Experiment 2/event.test.js new file mode 100644 index 000000000..65d98a4aa --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/event.test.js @@ -0,0 +1,221 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const context = experiment.context; + console.log("Testing Context of Experiment"); + console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`); + console.log(`Actual: ${JSON.stringify(context, null, 2)}`); + pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context)); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test created contexts", function() { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + + const expected_context = { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + } + ] + }; + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey3": "value3-control", + "pmTestKey4": "value3-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey3": "value4-test", + "pmTestKey4": "value4-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/request.json b/postman/experimentation-platform/Create Experiment 2/request.json new file mode 100644 index 000000000..d231f32da --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/request.json @@ -0,0 +1,78 @@ +{ + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "name": "experiment-1", + "override_keys": [ + "pmTestKey3", + "pmTestKey4" + ], + "traffic_percentage": 10, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + } + ] + }, + "variants": [ + { + "id": "control", + "variant_type": "CONTROL", + "overrides": { + "pmTestKey3": "value3-control", + "pmTestKey4": "value3-control" + } + }, + { + "id": "test1", + "variant_type": "EXPERIMENTAL", + "overrides": { + "pmTestKey3": "value4-test", + "pmTestKey4": "value4-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment 2/response.json b/postman/experimentation-platform/Create Experiment 2/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment 2/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Create Experiment/.event.meta.json b/postman/experimentation-platform/Create Experiment/.event.meta.json new file mode 100644 index 000000000..4ac527d83 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.test.js", + "event.prerequest.js" + ] +} diff --git a/postman/experimentation-platform/Create Experiment/event.prerequest.js b/postman/experimentation-platform/Create Experiment/event.prerequest.js new file mode 100644 index 000000000..da93399e2 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/event.prerequest.js @@ -0,0 +1,79 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey1`, + `pmTestKey2` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'x-tenant': 'test', + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +function create_dimensions() { + const dimensions = [ + {name: "os", priority: 10, schema: { type: "string", enum: ["android", "ios", "web"] }}, + {name: "client", priority: 100, schema: { type: "string", pattern: ".*" }}, + {name: "variantIds", priority: 1000, schema: { type: "string", pattern: ".*" }} + ]; + + for (const dimension of dimensions) { + const options = { + 'method': 'PUT', + 'url': `${host}/dimension`, + 'header': { + 'Authorization': `Bearer ${token}`, + 'x-tenant': 'test', + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "dimension": dimension.name, + "priority": dimension.priority, + "schema": dimension.schema + }) + } + }; + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating dimension: ${dimension.name}`); + console.log(error); + return; + } + console.log(`Created dimension: ${dimension.name}`); + }); + } +} + +create_default_config_keys(); +create_dimensions(); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/event.test.js b/postman/experimentation-platform/Create Experiment/event.test.js new file mode 100644 index 000000000..2acd7253e --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/event.test.js @@ -0,0 +1,221 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const context = experiment.context; + console.log("Testing Context of Experiment"); + console.log(`Expected: ${JSON.stringify(expected_context, null, 2)}`); + console.log(`Actual: ${JSON.stringify(context, null, 2)}`); + pm.expect(JSON.stringify(context)).to.be.eq(JSON.stringify(expected_context)); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test created contexts", function() { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + + const expected_context = { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + } + ] + }; + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey1": "value1-control", + "pmTestKey2": "value1-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey1": "value2-test", + "pmTestKey2": "value2-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_context, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/request.json b/postman/experimentation-platform/Create Experiment/request.json new file mode 100644 index 000000000..030e4c00f --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/request.json @@ -0,0 +1,83 @@ +{ + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "name": "experiment-1", + "override_keys": [ + "pmTestKey1", + "pmTestKey2" + ], + "traffic_percentage": 10, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac1" + ] + } + ] + }, + "variants": [ + { + "id": "control", + "variant_type": "CONTROL", + "overrides": { + "pmTestKey1": "value1-control", + "pmTestKey2": "value1-control" + } + }, + { + "id": "test1", + "variant_type": "EXPERIMENTAL", + "overrides": { + "pmTestKey1": "value2-test", + "pmTestKey2": "value2-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Create Experiment/response.json b/postman/experimentation-platform/Create Experiment/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Create Experiment/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Get Experiment/.event.meta.json b/postman/experimentation-platform/Get Experiment/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Get Experiment/event.test.js b/postman/experimentation-platform/Get Experiment/event.test.js new file mode 100644 index 000000000..f0f823aa3 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/event.test.js @@ -0,0 +1,3 @@ +pm.test("200 OK", function() { + pm.response.to.have.status(200); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Get Experiment/request.json b/postman/experimentation-platform/Get Experiment/request.json new file mode 100644 index 000000000..ddb344f65 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/request.json @@ -0,0 +1,20 @@ +{ + "method": "GET", + "header": [ + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Get Experiment/response.json b/postman/experimentation-platform/Get Experiment/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Get Experiment/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js new file mode 100644 index 000000000..f57077057 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/event.test.js @@ -0,0 +1,3 @@ +pm.test("200 check", function() { + pm.response.to.have.status(200); +}) \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json new file mode 100644 index 000000000..2c2789238 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/request.json @@ -0,0 +1,42 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Thu, 01 Jan 1970 00:00:00 +0000", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS,CONCLUDED", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS,CONCLUDED" + } + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Thu, 01 Jan 1970 00:00:00 +0000]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js new file mode 100644 index 000000000..e765cbe6c --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/event.test.js @@ -0,0 +1,3 @@ +pm.test("304 check", function() { + pm.response.to.have.status(304); +}) \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json new file mode 100644 index 000000000..e0df22d43 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/request.json @@ -0,0 +1,42 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "If-Modified-Since", + "value": "Wed, 01 Jan 2070 00:00:00 +0000", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [If-Modified-Since = Wed, 01 Dec 2070 00:00:00 +0000]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json new file mode 100644 index 000000000..3e863c985 --- /dev/null +++ b/postman/experimentation-platform/List experiments [No If-Modified-Since]/request.json @@ -0,0 +1,45 @@ +{ + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "url": { + "raw": "{{host}}/experiments?from_date=2023-01-01%2000%3A00%3A00%20UTC&to_date=2023-08-04%2005%3A52%3A39.889727%20UTC&page=1&count=100&status=CREATED,INPROGRESS", + "host": [ + "{{host}}" + ], + "path": [ + "experiments" + ], + "query": [ + { + "key": "from_date", + "value": "2023-01-01%2000%3A00%3A00%20UTC" + }, + { + "key": "to_date", + "value": "2023-08-04%2005%3A52%3A39.889727%20UTC" + }, + { + "key": "page", + "value": "1" + }, + { + "key": "count", + "value": "100" + }, + { + "key": "status", + "value": "CREATED,INPROGRESS" + } + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json b/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/List experiments [No If-Modified-Since]/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Ramp/.event.meta.json b/postman/experimentation-platform/Ramp/.event.meta.json new file mode 100644 index 000000000..688c85746 --- /dev/null +++ b/postman/experimentation-platform/Ramp/.event.meta.json @@ -0,0 +1,5 @@ +{ + "eventOrder": [ + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Ramp/event.test.js b/postman/experimentation-platform/Ramp/event.test.js new file mode 100644 index 000000000..e0ad446c2 --- /dev/null +++ b/postman/experimentation-platform/Ramp/event.test.js @@ -0,0 +1,38 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function fetch_experiment_n_test(experiment_id, expected_traffic_percentage) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + console.log(`Expected: ${expected_traffic_percentage}, Actual: ${experiment.traffic_percentage}`); + pm.expect(experiment.traffic_percentage).to.be.eq(expected_traffic_percentage); + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test traffic percentage", function() { + const experiment_id = pm.environment.get("experiment_id"); + + fetch_experiment_n_test(experiment_id, 46); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Ramp/request.json b/postman/experimentation-platform/Ramp/request.json new file mode 100644 index 000000000..9c4e4edc9 --- /dev/null +++ b/postman/experimentation-platform/Ramp/request.json @@ -0,0 +1,42 @@ +{ + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "traffic_percentage": 46 + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/ramp", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "ramp" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Ramp/response.json b/postman/experimentation-platform/Ramp/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Ramp/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/Update Override Keys/.event.meta.json b/postman/experimentation-platform/Update Override Keys/.event.meta.json new file mode 100644 index 000000000..2df9d47d9 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/.event.meta.json @@ -0,0 +1,6 @@ +{ + "eventOrder": [ + "event.prerequest.js", + "event.test.js" + ] +} diff --git a/postman/experimentation-platform/Update Override Keys/event.prerequest.js b/postman/experimentation-platform/Update Override Keys/event.prerequest.js new file mode 100644 index 000000000..49475ab42 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/event.prerequest.js @@ -0,0 +1,42 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + +function create_default_config_keys() { + let keys = [ + `pmTestKey1972`, + `pmTestKey1999` + ]; + + for (const key of keys) { + const options = { + 'method': 'PUT', + 'url': `${host}/default-config/${key}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'x-tenant': 'test', + 'Content-Type': 'application/json' + }, + "body": { + "mode": "raw", + "raw": JSON.stringify({ + "value": "value1", + "schema": { + "type": "string", + "pattern": ".*" + } + }) + } + }; + console.log(options); + pm.sendRequest(options, function (error, response) { + if (error) { + console.log(`Error creating default-config key: ${key}`); + console.log(error); + return; + }; + console.log(`Created default-config key: ${key}`); + }); + } +} + +create_default_config_keys() \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/event.test.js b/postman/experimentation-platform/Update Override Keys/event.test.js new file mode 100644 index 000000000..ddb6a0175 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/event.test.js @@ -0,0 +1,194 @@ +const host = pm.environment.get("host"); +const token = pm.environment.get("token"); + + +function fetch_context_n_test(context_id, expected_override_id, expected_override, expected_variant_context) { + const getRequest = { + url: `${host}/context/${context_id}`, + method: 'GET', + header: { + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + + pm.sendRequest(getRequest, (error, response) => { + if(error) { + console.log("Failed to fetch context"); + throw error; + } + + const context = response.json(); + + /*********** checking contexts created in CAC **********/; + + + const variant_override_id = context.override_id; + const varaint_context = context.value; + const variant_override = context.override; + + console.log("Testing variant override id"); + console.log("Override from CAC: \n", variant_override_id); + console.log("Expected Context: \n", expected_override_id); + pm.expect(variant_override_id).to.be.eq(expected_override_id); + + console.log("Testing variant override"); + console.log("Override from CAC: \n", JSON.stringify(variant_override, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_override, null, 2)); + pm.expect(JSON.stringify(variant_override)).to.be.eq(JSON.stringify(expected_override)); + + console.log("Testing variant context"); + console.log("Context from CAC: \n", JSON.stringify(varaint_context, null, 2)); + console.log("Expected Context: \n", JSON.stringify(expected_variant_context, null, 2)); + pm.expect(JSON.stringify(varaint_context)).to.be.eq(JSON.stringify(expected_variant_context)); + }); +} + +function fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts) { + const options = { + 'method': 'GET', + 'url': `${host}/experiments/${experiment_id}`, + "header": { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-tenant': 'test', + } + }; + + pm.sendRequest(options, function(error, response) { + if(error) { + console.log("Failed to fetch experiment"); + throw error; + } + + const experiment = response.json(); + + const variants = experiment.variants; + for(const variant of variants) { + const variant_id = variant.id; + + console.log(`TESTING variant: ${variant_id}`); + + // check if the variant present in the expected_variants + const variant_cpy = JSON.parse(JSON.stringify(variant)); + delete variant_cpy.override_id; + delete variant_cpy.context_id; + + const expected_variant = expected_varaints.find((ev) => ev.id === variant_id); + console.log("Actual Variant:", JSON.stringify(variant_cpy, null, 4)); + console.log("Expected Variant:", JSON.stringify(expected_variant, null, 4)); + pm.expect(JSON.stringify(variant_cpy)).to.be.eq(JSON.stringify(expected_variant)); + + /*********/ + + const expected_context_id = variant.context_id; + const expected_override_id = variant.override_id; + const expected_override = variant.overrides; + const expected_variant_context = expected_variant_contexts.find(evc => evc.vid === variant_id)?.context; + + fetch_context_n_test(expected_context_id, expected_override_id, expected_override, expected_variant_context); + } + }); +} + +// check experiment creation in experiment +pm.test("200 OK", function () { + const response = pm.response.json(); + const experiment_id = response.experiment_id; + + pm.environment.set("experiment_id", experiment_id); + pm.response.to.have.status(200); +}); + + +// check for contexts in CAC +pm.test("Test updated experiment", function() { + const response = pm.response.json(); + const experiment_id = response.id; + + const expected_varaints = [ + { + "id": `${experiment_id}-control`, + "overrides": { + "pmTestKey1972": "value-7910-an-control", + "pmTestKey1999": "value-6910-an-control" + }, + "variant_type": "CONTROL" + }, + { + "id": `${experiment_id}-test1`, + "overrides": { + "pmTestKey1972": "value-7920-an-test", + "pmTestKey1999": "value-6930-an-test" + }, + "variant_type": "EXPERIMENTAL" + } + ]; + const expected_variant_contexts = [ + { + "vid": `${experiment_id}-control`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-control`, + { + "var": "variantIds" + } + ] + } + ] + } + }, + { + "vid": `${experiment_id}-test1`, + "context": { + "and": [ + { + "==": [ + { + "var": "os" + }, + "ios" + ] + }, + { + "==": [ + { + "var": "client" + }, + "testClientCac02" + ] + }, + { + "in": [ + `${experiment_id}-test1`, + { + "var": "variantIds" + } + ] + } + ] + } + } + ]; + + fetch_experiment_n_test(experiment_id, expected_varaints, expected_variant_contexts); +}); \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/request.json b/postman/experimentation-platform/Update Override Keys/request.json new file mode 100644 index 000000000..145614cf2 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/request.json @@ -0,0 +1,56 @@ +{ + "method": "PUT", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "default" + }, + { + "key": "x-tenant", + "value": "test", + "type": "default" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "override_keys": [ + "pmTestKey1972", + "pmTestKey1999" + ], + "variants": [ + { + "id": "{{experiment_id}}-control", + "overrides": { + "pmTestKey1972": "value-7910-an-control", + "pmTestKey1999": "value-6910-an-control" + } + }, + { + "id": "{{experiment_id}}-test1", + "overrides": { + "pmTestKey1972": "value-7920-an-test", + "pmTestKey1999": "value-6930-an-test" + } + } + ] + } + }, + "url": { + "raw": "{{host}}/experiments/{{experiment_id}}/overrides", + "host": [ + "{{host}}" + ], + "path": [ + "experiments", + "{{experiment_id}}", + "overrides" + ] + } +} \ No newline at end of file diff --git a/postman/experimentation-platform/Update Override Keys/response.json b/postman/experimentation-platform/Update Override Keys/response.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/postman/experimentation-platform/Update Override Keys/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/experimentation-platform/event.prerequest.js b/postman/experimentation-platform/event.prerequest.js new file mode 100644 index 000000000..e69de29bb diff --git a/postman/experimentation-platform/event.test.js b/postman/experimentation-platform/event.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..5ac7f874c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.76.0" +targets = ["wasm32-unknown-unknown"] +components = ["rust-src", "rustfmt", "clippy", "rust-analyzer"] diff --git a/rust.nix b/rust.nix new file mode 100644 index 000000000..11b179860 --- /dev/null +++ b/rust.nix @@ -0,0 +1,71 @@ +{ inputs, ... }: +{ + perSystem = { config, self', pkgs, lib, system, ... }: + let + rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain; + craneArgs = { + pname = "superposition"; + version = "0.0.1"; + src = ./.; + buildInputs = lib.optionals pkgs.stdenv.isDarwin + (with pkgs.darwin.apple_sdk.frameworks; [ + Security + SystemConfiguration + pkgs.fixDarwinDylibNames + ]) ++ [ + pkgs.libiconv + pkgs.openssl + pkgs.postgresql_12 + ]; + nativeBuildInputs = [ + pkgs.pkg-config + ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly craneArgs; + package = craneLib.buildPackage (craneArgs // { + inherit cargoArtifacts; + # https://discourse.nixos.org/t/how-to-use-install-name-tool-on-darwin/9931/2 + postInstall = '' + ${if pkgs.stdenv.isDarwin then "fixDarwinDylibNames" else ""} + ''; + }); + + check = craneLib.cargoClippy (craneArgs // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features -- --deny warnings"; + }); + in + { + _module.args.pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ inputs.rust-overlay.overlays.default ]; + + }; + + packages.superposition = package; + + checks.clippy = check; + + # Flake outputs + devShells.rust = pkgs.mkShell { + inputsFrom = [ + package # Makes the buildInputs of the package available in devShell (so cargo can link against Nix libraries) + ]; + shellHook = '' + # For rust-analyzer 'hover' tooltips to work. + export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"; + ''; + nativeBuildInputs = with pkgs; [ + # Add your dev tools here. + bacon + cargo-watch + diesel-cli + leptosfmt + rustToolchain + wasm-pack + ] ++ craneArgs.nativeBuildInputs; + }; + }; +} \ No newline at end of file diff --git a/scripts/context_id_update.py b/scripts/context_id_update.py new file mode 100644 index 000000000..1ecaf8ccf --- /dev/null +++ b/scripts/context_id_update.py @@ -0,0 +1,35 @@ +import requests +import json +import os + + +context_url = os.getenv("CONTEXT_URL","http://localhost:8080/context") +config_url = os.getenv("CONFIG_URL","http://localhost:8080/config") +cac_config = requests.get(url=config_url, headers={"x-tenant": "mjos"}).json() + +print(cac_config) + +for i in cac_config["contexts"]: + context_id = i["id"] + move_context_data = { + "context": i["condition"] + } + + update_context_data = { + "context": i["condition"], + "override": cac_config["overrides"][i["override_with_keys"][0]] + } + move_context = requests.put(url=context_url + "/move/" + str(context_id), json=move_context_data, headers={"x-tenant": "mjos", "Authorisation": "12345678"}) + if move_context.status_code == 200: + print("move context request success\n") + print(move_context.json()) + else: + print("move context request failed\n") + print(json.dumps(move_context_data)) + update_context = requests.put(url=context_url, json=update_context_data, headers={"x-tenant": "mjos", "Authorisation": "12345678"}) + if update_context.status_code == 200: + print("update context request success\n") + print(update_context.json()) + else: + print("update context request failed\n") + print(json.dumps(update_context_data)) diff --git a/scripts/create-tenant.sh b/scripts/create-tenant.sh new file mode 100755 index 000000000..913d5c423 --- /dev/null +++ b/scripts/create-tenant.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +shopt -s extglob + +TENANT=$1 +DB_URL=$2 + +echo "Tenant ID ==> $TENANT" +echo "DB URL ==> $DB_URL" + +# Creating schemas +CAC_SCHEMA="${TENANT}_cac" +EXP_SCHEMA="${TENANT}_experimentation" + +function generate_sql() { + service=$1 + schema=$2 + + rm ${schema}.sql + + for f in $(find "crates/$service/migrations" -name "up.sql" | grep -v "diesel_initial_setup" | sort) + do + OLDIFS=$IFS + IFS= + sql="$(cat $f | sed "s/public/${schema}/g")" + echo $sql >> "${schema}.sql" + IFS=$OLDIFS + done + + echo "Generated ${schema}.sql" + + echo "Running migrations for $schema" + psql "$DB_URL" -f ${schema}.sql +} + +generate_sql "context_aware_config" $CAC_SCHEMA +generate_sql "experimentation_platform" $EXP_SCHEMA +psql "$DB_URL" -c "INSERT INTO $CAC_SCHEMA.dimensions (dimension, priority, created_at, created_by, schema, function_name) VALUES ('variantIds', 1, CURRENT_TIMESTAMP, 'anon@juspay.in', '{\"type\": \"string\",\"pattern\": \".*\"}'::json, null);" + +shopt -u extglob \ No newline at end of file diff --git a/scripts/legacy-db-setup.sh b/scripts/legacy-db-setup.sh new file mode 100755 index 000000000..35380e805 --- /dev/null +++ b/scripts/legacy-db-setup.sh @@ -0,0 +1,17 @@ +DB_URL=$1 + +cp -r "crates/context_aware_config/migrations/." "crates/context_aware_config/cac_v1_migrations" +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; + +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec cat {} \; + +xargs cp -r "crates/experimentation_platform/migrations/." "crates/experimentation_platform/cac_v1_migrations" +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec sed -i'' "s/public/cac_v1/g" {} \; + +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec cat {} \; + +find "crates/context_aware_config/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; +find "crates/experimentation_platform/cac_v1_migrations" -name "up.sql" -exec psql "$DB_URL" -f {} \; + +rm -rf "crates/context_aware_config/cac_v1_migrations" +rm -rf "crates/experimentation_platform/cac_v1_migrations" \ No newline at end of file diff --git a/scripts/regex.hs b/scripts/regex.hs new file mode 100644 index 000000000..b95569c0c --- /dev/null +++ b/scripts/regex.hs @@ -0,0 +1,204 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE MultiWayIf #-} +{-# OPTIONS_GHC -Wno-name-shadowing #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-unused-imports #-} + +import System.Environment +import qualified Data.Text as T +import GHC.Generics +import qualified GHC.Base as G +import Data.Aeson +import Data.Maybe + + + +data Platform + = ANDROID + | IOS + | WEB + deriving (Generic, Show, Eq, Ord, Enum, Read) + +instance FromJSON Platform + where + parseJSON (String a) = genericParseJSON defaultOptions $ String (T.toUpper a) + parseJSON _ = fail "Invalid Platform" + +instance ToJSON Platform where + toJSON = String . T.toLower . T.pack . show + +createPathRegex :: T.Text -> T.Text -> Platform -> T.Text +createPathRegex "in.juspay.godel.placeholder" asset platform = createPathRegex "in.juspay.godel" asset platform +createPathRegex app asset platform = createPathRegex' $ + case asset of + "index" -> case platform of + WEB -> [stdIndexPathCmn, newIndexPathCmn] + _ -> [stdIndexPathApp, stdIndexPathCmn, newIndexPathCmn] + + "manifest" -> (: [stdManifestPath]) $ case platform of + WEB -> ["hyper", "bundles", "web", "release"] + IOS -> ["juspay", "payments", "release", "IOS"] + ANDROID -> ["juspay", "payments", "release"] + + "fonts" -> [[".*"]] + + "sdk_config" -> [[".*"]] + + "tracker" -> (: [["hyper", "bundles", "app", "tracker", semVerRegex]]) $ case platform of + WEB -> ["hyper", "bundles", "web", "release", createMappRegex "in.juspay.hyperos", sdkV2Regex', clientIdRegex, "(stable|" <> assetVerRegex <> "|staggered)"] + _ -> ["juspay", "payments", "2\\.0", "release(\\/" <> assetVerRegex <> ")?"] + + "config" -> case (app, platform) of + ("in.juspay.godel", _) -> [[".*"]] + ("in.juspay.hyperos", WEB) -> [[".*"]] + ("in.juspay.hyperos", _) -> [["juspay", "payments", "2\\.0", "release(\\/"<> assetVerRegex <> ")?"]] + (_, WEB) -> [stdAssetPathCmn, newConfigPathCmn] + _ -> [stdAssetPathCmn, stdAssetPathApp, newConfigPathCmn] + + _ -> case (app, platform) of + ("in.juspay.godel", _) -> [[".*"]] + (_, WEB) -> [stdAssetPathCmn, galactusAssetPath, galactusAssetPathV1, galactusAssetPathV1', newConfigPathCmn] + ("in.yatri.consumer",_) -> [preReleaseAssetPath, galactusAssetPathV1'] + ("in.yatri.provider",_) -> [preReleaseAssetPath, galactusAssetPathV1'] + _ -> [stdAssetPathCmn, stdAssetPathApp, galactusAssetPath, galactusAssetPathV1, galactusAssetPathV1', newConfigPathCmn] + + where + createPathRegex' :: [[T.Text]] -> T.Text + createPathRegex' patterns = + if asset /= "index" && app == "in.juspay.escrow" + then (createPathRegex'' patterns) <> "|" <> (createPathRegex "in.juspay.hyperpay" asset platform) + else createPathRegex'' patterns + + createPathRegex'' :: [[T.Text]] -> T.Text + createPathRegex'' = T.intercalate "|" . G.map ((bucketPath <>) . T.intercalate "\\/" . (++ [fileName <> "$"])) + + bucketPath = + case app of + "in.juspay.godel" -> "^https:\\/\\/.*\\/" + _ -> "^https:\\/\\/" <> (T.replace "." "\\." getReadAssetsDomain) <> "\\/" + + stdManifestPath = ["hyper", "bundles", "in\\.juspay\\.merchants", clientIdRegex, platform', "(cug|release)"] + + platform' = T.toLower . T.pack $ show platform + + ext = "\\." <> (getExtensionForAsset platform app asset) + + lookupAppFName "com.juspay.gemi" = \case + "strings" -> Just $ "strings(_[a-z,A-Z,0-9]+)?\\." <> ext + fname -> Just $ fname <> "\\." <> ext + lookupAppFName _ = const Nothing + + platformPrefix = case platform of + WEB -> "" + _ -> "v1-" + + getDefaultFName fname = platformPrefix <> fname <> ext + + fileName = + case asset of + "manifest" -> "manifest" <> ext + "sdk_config" -> "sdk_config" <> ext + "fonts" -> "(.*)" <> ext + "certificates" -> "certificates_v1" <> ext + "acs_js_source" -> getDefaultFName "acs" + "boot_loader_js_source" -> getDefaultFName "boot_loader" + "index" -> case platform of + WEB -> getDefaultFName "(prod-split_)?(br[0-9]+-)?index[0-9]*" + _ -> getDefaultFName "index_bundle" + _ -> let mbAppFName = lookupAppFName app asset + in fromMaybe (getDefaultFName asset) mbAppFName + + clientIdRegex = "[a-z,0-9]+((\\.|,|-|_)[a-z,0-9]+)*" + branchRegex = "[^\\/]+" + + scope = + case platform of + IOS -> "release\\/IOS" + _ -> "release" + + sdkV1Regex = "1\\.0rc1" + sdkV2Regex = "2\\.0rc1" + sdkV2Regex' = "2\\.0\\.0" + assetVerRegex = "([0-9].){2}[0-9]+" + semVerRegex = "([0-9]+\\.){2}[0-9]+" + calVerRegex = semVerRegex <> "-(release|hotfix)(-[a-z0-9]+)?-[0-9]+(-[0-9]+)?\\.[0-9]+" + preReleaseVerRegex = semVerRegex <> "-(main)(-"<>clientIdRegex <>")?\\.[0-9]+" + + mappRegex = createMappRegex app + + createMappRegex = (<> ("(\\." <> clientIdRegex <> ")?")) . T.replace "." "\\." + + indexV1Regex = sdkV1Regex <> "_[0-9]+" + + indexV2Regex = sdkV2Regex <> "_[0-9]+" + + newIndexPathPrefix = ["hyper", "bundles", "app", mappRegex] + newIndexPathCmn = newIndexPathPrefix <> case app of + "com.juspay.gemi" -> [branchRegex, clientIdRegex, platform'] + _ -> [versionRegex, platform'] + where + versionList = [indexV1Regex, indexV2Regex, semVerRegex, calVerRegex] + versionRegex = "(" <> (T.intercalate "|" versionList) <> ")" + + stdIndexPathCmn = + case app of + "in.juspay.hypercredit" -> stdIndexPathCmn' indexV1Regex + _ -> stdIndexPathCmn' indexV2Regex + where + stdIndexPathCmn' indexVRegex = ["hyper", "bundles", platform', "release", mappRegex, sdkV2Regex', clientIdRegex, "(stable|" <> indexVRegex <> "|staggered)"] + stdIndexPathApp = + case app of + "in.juspay.arya" -> ["juspay", mappRegex, scope, sdkV1Regex, indexV1Regex] + "in.juspay.hypercredit" -> stdIndexPathApp' indexV1Regex + "neopenkochi.yatri" -> becknPath + "neopenkochi.yatripartner" -> becknPath + _ -> stdIndexPathApp' indexV2Regex + where + becknPath = ["juspay", "beckn", mappRegex, scope, sdkV1Regex, indexV1Regex] + stdIndexPathApp' indexVRegex = ["juspay", "payments", mappRegex, scope, sdkV2Regex <> "(\\/(" <> indexVRegex <> "|" <> "staggered-release-assets))?"] + + stdAssetPathCmn = ["hyper", "bundles", platform', "release", mappRegex, sdkV2Regex', clientIdRegex, "(stable|" <> assetVerRegex <> "|staggered)"] + + newConfigPathCmn = ["hyper", "bundles", "config", mappRegex, clientIdRegex, versionRegex] + where + versionRegex = "(" <> (T.intercalate "|" [calVerRegex, indexV2Regex, indexV1Regex, semVerRegex]) <> ")" + + stdAssetPathApp = ["juspay", "payments", mappRegex, "release" <> (if platform == IOS then "(\\/IOS)?" else "") <> "(\\/(" <> indexV2Regex <> "|" <> assetVerRegex <> "|" <> "staggered-release-assets))?"] + + galactusAssetPath = ["hyper", "bundles", createMappRegex "in.juspay.merchants", clientIdRegex, "configuration", "[0-9]+\\.[0-9]+"] + galactusAssetPathV1' = ["hyper", "configs", clientIdRegex, mappRegex, semVerRegex] + preReleaseAssetPath = ["hyper", "configs", clientIdRegex, mappRegex, preReleaseVerRegex] + + galactusAssetPathV1 = ["hyper", "configs", clientIdRegex, mappRegex, semVerRegex, "configuration"] + + getReadAssetsDomain :: T.Text + getReadAssetsDomain = T.pack "assets.juspay.in" + + getExtensionForAsset :: Platform -> T.Text -> T.Text -> T.Text + getExtensionForAsset platform app asset = + case (app, asset) of + ("com.juspay.gemi", asset) | asset /= "index" -> "json" + (_ , "sdk_config") -> "json" + (_ , "manifest") -> "json" + (_ , "fonts") -> "ttf" + _ -> getDefaultFNameExt platform + + getDefaultFNameExt :: Platform -> T.Text + getDefaultFNameExt = + \case + WEB -> "js" + IOS -> "jsa" + ANDROID -> "zip" + + +main :: IO () +main = do + args <- getArgs + case args of + [arg1, arg2] -> do + let android = createPathRegex (T.pack arg1) (T.pack arg2) ANDROID + let ios = createPathRegex (T.pack arg1) (T.pack arg2) IOS + let web = createPathRegex (T.pack arg1) (T.pack arg2) WEB + putStrLn (show (android <> (T.pack "|") <> ios <> (T.pack "|") <> web)) + _ -> error "Provide proper Haskell <argument>" \ No newline at end of file diff --git a/backend/scripts/server-setup.sh b/scripts/server-setup.sh similarity index 90% rename from backend/scripts/server-setup.sh rename to scripts/server-setup.sh index 1f9bd4646..e93b6dd36 100644 --- a/backend/scripts/server-setup.sh +++ b/scripts/server-setup.sh @@ -36,3 +36,5 @@ cargo add diesel --features "diesel/postgres diesel/r2d2 diesel/serde_json diese cargo build echo "--------cargo build finished-------------" +echo "------------mischellaneous---------------" +touch docker-compose/localstack/export_cyphers.sh diff --git a/scripts/update_schema.js b/scripts/update_schema.js new file mode 100644 index 000000000..a034587ce --- /dev/null +++ b/scripts/update_schema.js @@ -0,0 +1,170 @@ +import fetch from "node-fetch"; +import cp from "child_process"; + +let host = "http://localhost:8080" // change it respective host + +// Api calls +// Using a local api to get the app_name and asset from the key_name (getApp) + +async function getConfig() { + try { + const response = await fetch(`${host}/config`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + }); + let config = await response.json(); + switch(response.status) { + case 200: + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function getApp(key, val) { + try { + let body = {"key": key, "val": val}; + const response = await fetch(`http://localhost:8080/config/getapp`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + body: JSON.stringify(body), + }); + switch(response.status) { + case 200: + let config = await response.json(); + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function updateRegex(key, isArray, pattern) { + try { + let body = {}; + + if (isArray) { + body = {"schema":{"type": ["null", "array"],"items": {"type": "string", "pattern": pattern}}}; + } else { + body = {"schema":{"type": ["null", "string"], "pattern": pattern}}; + } + + const response = await fetch(`${host}/default-config/${key}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer 12345678" + }, + body: JSON.stringify(body), + }); + switch(response.status) { + case 200: + let config = await response.text(); + return config; + default: + const errorText = await response.text(); + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function getRegex(app, asset) { + try { + let resp = cp.execSync(`runghc regex.hs ${app} ${asset}`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); + return resp; + } catch (error) { + console.error(error); + throw error; + } +} + +async function getRegexForValue(key, value) { + let regex = ""; + console.log(key); + if (typeof value !== "string") { + regex = "^(default_array_ignore_this|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|in.juspay.arya|in.juspay.hypercredit|in.juspay.godel|in.juspay.godel.placeholder|in.juspay.hyperapi|in.juspay.hyperupi|in.juspay.inappupi|in.juspay.vies|net.openkochi.yatri|net.openkochi.yatripartner|in.juspay.hyperpay.placeholder)$"; + let update_response = await updateRegex(key, true, regex); + console.log(update_response); // + + return Promise.resolve(update_response); + } else { + if (key.endsWith("etag")) { + regex = "^default_str_ignore_this$|^(?=.*[a-zA-Z])(?=.*[0-9])[A-Za-z0-9]+$"; + } else { + try { + let resp = await getApp(key, value); + let regex_from_galactus = await getRegex(resp.app_name, resp.asset); + + let add_default_regex = "^default_str_ignore_this$|" + JSON.parse(regex_from_galactus); + regex = add_default_regex; + + } catch (error) { + console.error(error); + throw error; + } + } + console.log(regex); + let update_response = await updateRegex(key, false, regex); + console.log(update_response); // + + return Promise.resolve(update_response); + } + +} + +async function decideRegex(i, default_configs) { + if (i >= Object.keys(default_configs).length){ + return Promise.resolve("done"); + } + let key = Object.keys(default_configs)[i]; + let value = Object.values(default_configs)[i]; + + try { + let regex_response = await getRegexForValue(key, value); + if (regex_response) { + return await decideRegex(i+1, default_configs); + } else { + return Promise.reject("notDone"); + } + } catch (error) { + console.error(error); + throw error; + } +} + +async function heartbeat(){ + let config = await getConfig(); + let default_configs = config.default_configs; + + let keysToFilter = ["pmTestKey1972", "pmTestKey1999"]; + keysToFilter.forEach(key => { + delete default_configs[key]; + }); + + try { + await decideRegex(0, default_configs); + } catch (error) { + console.error(error); + throw error; + } +} + +heartbeat(); diff --git a/setup.sh b/setup.sh new file mode 100644 index 000000000..3885846e2 --- /dev/null +++ b/setup.sh @@ -0,0 +1,3 @@ +#!bin/sh +make setup && make tenant TENANT=mjos && make tenant TENANT=sdk_config +npm i \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index a10978834..000000000 --- a/src/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as jsonLogic from 'json-logic-js'; -import {deepMerge} from './utils/deepMerge'; -import {compareSemanticIsGreater} from './utils/operations' -import {IObject, Dimension, DimensionConfig} from './types' - - -export class Config{ - - dimension : Array<Dimension>; - overrides : IObject; - defaultConfig : IObject; - - - static { - jsonLogic.add_operation(">>",compareSemanticIsGreater); - } - - constructor(dimension : DimensionConfig, overrides : IObject, defaultConfig : IObject) { - this.dimension = dimension.dimensions; - this.overrides = overrides; - this.defaultConfig = defaultConfig; - } - - - public evaluateConfig(data : IObject) : IObject { - - const requiredOverrides : Array<IObject> = []; - for(let i = 0; i < this.dimension.length; i++) { - if(jsonLogic.apply(this.dimension[i].condition, data)) { - requiredOverrides.push( - ...this.dimension[i].overrideWithKeys.map(x => this.overrides[x]) - ); - } - } - - const targetConfig : IObject = {...this.defaultConfig}; - return deepMerge(targetConfig , ...requiredOverrides); - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index fef70d725..000000000 --- a/src/types.ts +++ /dev/null @@ -1,19 +0,0 @@ - -export type IObject = { - [key: string]: any; -} - -export type Dimension = { - condition : IObject, - overrideWithKeys : Array<string> -} - -export type DimensionConfig = { - dimensions : Array<Dimension> -}; - - -export type IIsObject = { - (item: any): boolean; -} - diff --git a/src/utils/deepMerge.ts b/src/utils/deepMerge.ts deleted file mode 100644 index e5ed19eda..000000000 --- a/src/utils/deepMerge.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {IIsObject, IObject} from '../types' - - -const isObject: IIsObject = (item: any): boolean => { - return (item === Object(item) && !Array.isArray(item)); -}; - -export const deepMerge = (target: IObject, ...sources: Array<IObject>) : IObject => { - // return the target if no sources passed - if (!sources.length) { - return target; - } - - const result: IObject = target; - - if (isObject(result)) { - const len: number = sources.length; - - for (let i = 0; i < len; i += 1) { - const elm: any = sources[i]; - - if (isObject(elm)) { - for (const key in elm) { - if (elm.hasOwnProperty(key)) { - if (isObject(elm[key])) { - if (!result[key] || !isObject(result[key])) { - result[key] = {}; - } - deepMerge(result[key], elm[key]); - } else { - if (Array.isArray(result[key]) && Array.isArray(elm[key])) { - result[key] = [...elm[key]]; - } else { - result[key] = elm[key]; - } - } - } - } - } - } - } - - return result; -}; diff --git a/src/utils/operations.ts b/src/utils/operations.ts deleted file mode 100644 index c37c4838f..000000000 --- a/src/utils/operations.ts +++ /dev/null @@ -1,15 +0,0 @@ -const compareSemanticIsGreaterImp = function(version_a : string, version_b : string) { - const x = version_a.split("."); - const y = version_b.split("."); - for(let i = 0; i < x.length; i++) { - if(x[i] > y[i]) - return true; - } - return false; -} - -export const compareSemanticIsGreater = function(version_a : string, version_b: string, version_c ?: string) { - return (version_c === undefined ? - compareSemanticIsGreaterImp(version_a, version_b) : - (compareSemanticIsGreaterImp(version_a, version_b) && compareSemanticIsGreaterImp(version_b, version_c))); -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 92f153064..000000000 --- a/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "commonjs", - "lib": ["es6"], - "allowJs": false, - "outDir": "build", - "rootDir": "src", - "strict": true, - "noImplicitAny": true, - "esModuleInterop": true, - "resolveJsonModule": true - } -}